/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) version 3. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with the program; if not, see * * * Authors: * Damon Chaplin * Rodrigo Moya * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * */ #ifdef HAVE_CONFIG_H #include #endif #include "e-meeting-time-sel.h" #include #include #include #include #include #include #include #include #include "e-meeting-utils.h" #include "e-meeting-list-view.h" #include "e-meeting-time-sel-item.h" #define E_MEETING_TIME_SELECTOR_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), E_TYPE_MEETING_TIME_SELECTOR, EMeetingTimeSelectorPrivate)) struct _EMeetingTimeSelectorPrivate { gboolean use_24_hour_format; }; /* An array of hour strings for 24 hour time, "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" }; /* An array of hour strings for 12 hour time, "12:00am" .. "11:00pm". */ const gchar *EMeetingTimeSelectorHours12[24] = { "12:00am", "1:00am", "2:00am", "3:00am", "4:00am", "5:00am", "6:00am", "7:00am", "8:00am", "9:00am", "10:00am", "11:00am", "12:00pm", "1:00pm", "2:00pm", "3:00pm", "4:00pm", "5:00pm", "6:00pm", "7:00pm", "8:00pm", "9:00pm", "10:00pm", "11:00pm" }; /* The number of days shown in the entire canvas. */ #define E_MEETING_TIME_SELECTOR_DAYS_SHOWN 35 #define E_MEETING_TIME_SELECTOR_DAYS_START_BEFORE 7 #define E_MEETING_TIME_SELECTOR_FB_DAYS_BEFORE 7 #define E_MEETING_TIME_SELECTOR_FB_DAYS_AFTER 28 /* 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 enum { PROP_0, PROP_USE_24_HOUR_FORMAT }; enum { CHANGED, LAST_SIGNAL }; static gint signals[LAST_SIGNAL] = { 0 }; static void e_meeting_time_selector_alloc_named_color (EMeetingTimeSelector * mts, const gchar *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_draw_key_color (GtkWidget *darea, cairo_t *cr, GdkColor *color); 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_draw (GtkWidget *widget, cairo_t *cr); static void e_meeting_time_selector_draw_shadow (EMeetingTimeSelector *mts, cairo_t *cr); 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 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, gboolean *push_in, gpointer user_data); static void e_meeting_time_selector_on_zoomed_out_toggled (GtkCheckMenuItem *button, EMeetingTimeSelector *mts); static void e_meeting_time_selector_on_working_hours_toggled (GtkCheckMenuItem *menuitem, 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, gboolean *push_in, 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 (EMeetingTime *start, EMeetingTime *end, gint *days, gint *hours, gint *minutes); static void e_meeting_time_selector_find_nearest_interval (EMeetingTimeSelector *mts, EMeetingTime *start_time, EMeetingTime *end_time, gint days, gint hours, gint mins); static void e_meeting_time_selector_find_nearest_interval_backward (EMeetingTimeSelector *mts, EMeetingTime *start_time, EMeetingTime *end_time, gint days, gint hours, gint mins); static void e_meeting_time_selector_adjust_time (EMeetingTime *mtstime, gint days, gint hours, gint minutes); static EMeetingFreeBusyPeriod * e_meeting_time_selector_find_time_clash (EMeetingTimeSelector *mts, EMeetingAttendee *attendee, EMeetingTime *start_time, EMeetingTime *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, EMeetingTime *mtstime); static void e_meeting_time_selector_restore_position (EMeetingTimeSelector *mts, EMeetingTime *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_canvas_size_allocate (GtkWidget *widget, GtkAllocation *allocation, EMeetingTimeSelector *mts); static void e_meeting_time_selector_update_main_canvas_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 gboolean e_meeting_time_selector_on_canvas_scroll_event (GtkWidget *widget, GdkEventScroll *event, EMeetingTimeSelector *mts); static gboolean e_meeting_time_selector_on_canvas_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data); static void row_inserted_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data); static void row_changed_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data); static void row_deleted_cb (GtkTreeModel *model, GtkTreePath *path, gpointer data); static void free_busy_template_changed_cb (EMeetingTimeSelector *mts); G_DEFINE_TYPE_WITH_CODE ( EMeetingTimeSelector, e_meeting_time_selector, GTK_TYPE_TABLE, G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)) static void meeting_time_selector_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_USE_24_HOUR_FORMAT: e_meeting_time_selector_set_use_24_hour_format ( E_MEETING_TIME_SELECTOR (object), g_value_get_boolean (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void meeting_time_selector_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_USE_24_HOUR_FORMAT: g_value_set_boolean ( value, e_meeting_time_selector_get_use_24_hour_format ( E_MEETING_TIME_SELECTOR (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void meeting_time_selector_dispose (GObject *object) { EMeetingTimeSelector *mts; mts = E_MEETING_TIME_SELECTOR (object); e_meeting_time_selector_remove_timeout (mts); if (mts->model) { g_signal_handlers_disconnect_matched ( mts->model, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, mts); g_object_unref (mts->model); mts->model = NULL; } mts->display_top = NULL; mts->display_main = NULL; if (mts->fb_refresh_not != 0) { g_source_remove (mts->fb_refresh_not); mts->fb_refresh_not = 0; } if (mts->style_change_idle_id != 0) { g_source_remove (mts->style_change_idle_id); mts->style_change_idle_id = 0; } /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (e_meeting_time_selector_parent_class)->dispose (object); } static void e_meeting_time_selector_class_init (EMeetingTimeSelectorClass *class) { GObjectClass *object_class; GtkWidgetClass *widget_class; g_type_class_add_private (class, sizeof (EMeetingTimeSelectorPrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = meeting_time_selector_set_property; object_class->get_property = meeting_time_selector_get_property; object_class->dispose = meeting_time_selector_dispose; widget_class = GTK_WIDGET_CLASS (class); 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->draw = e_meeting_time_selector_draw; g_object_class_install_property ( object_class, PROP_USE_24_HOUR_FORMAT, g_param_spec_boolean ( "use-24-hour-format", "Use 24-Hour Format", NULL, TRUE, G_PARAM_READWRITE)); signals[CHANGED] = g_signal_new ( "changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EMeetingTimeSelectorClass, changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void e_meeting_time_selector_init (EMeetingTimeSelector *mts) { mts->priv = E_MEETING_TIME_SELECTOR_GET_PRIVATE (mts); /* The shadow is drawn in the border so it must be >= 2 pixels. */ gtk_container_set_border_width (GTK_CONTAINER (mts), 2); mts->accel_group = gtk_accel_group_new (); 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 = TRUE; mts->dragging_position = E_MEETING_TIME_SELECTOR_POS_NONE; mts->list_view = NULL; mts->fb_refresh_not = 0; mts->style_change_idle_id = 0; e_extensible_load_extensions (E_EXTENSIBLE (mts)); } void e_meeting_time_selector_construct (EMeetingTimeSelector *mts, EMeetingStore *ems) { GtkWidget *hbox, *vbox, *separator, *label, *table, *sw; GtkWidget *alignment, *child_hbox, *arrow, *menuitem; GtkWidget *child; GtkAdjustment *adjustment; GtkScrollable *scrollable; GSList *group; guint accel_key; time_t meeting_start_time; struct tm *meeting_start_tm; AtkObject *a11y_label, *a11y_date_edit; /* 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_t (&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 = 17; mts->col_width = 55; mts->day_width = 55 * 24 + 1; mts->auto_scroll_timeout_id = 0; vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_table_attach ( GTK_TABLE (mts), vbox, 0, 1, 0, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); gtk_widget_show (vbox); mts->attendees_vbox_spacer = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_box_pack_start (GTK_BOX (vbox), mts->attendees_vbox_spacer, FALSE, FALSE, 0); gtk_widget_show (mts->attendees_vbox_spacer); mts->attendees_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_box_pack_start (GTK_BOX (vbox), mts->attendees_vbox, TRUE, TRUE, 0); gtk_widget_show (mts->attendees_vbox); /* build the etable */ mts->model = ems; if (mts->model) g_object_ref (mts->model); g_signal_connect_swapped ( mts->model, "notify::free-busy-template", G_CALLBACK (free_busy_template_changed_cb), mts); g_signal_connect ( mts->model, "row_inserted", G_CALLBACK (row_inserted_cb), mts); g_signal_connect ( mts->model, "row_changed", G_CALLBACK (row_changed_cb), mts); g_signal_connect ( mts->model, "row_deleted", G_CALLBACK (row_deleted_cb), mts); mts->list_view = e_meeting_list_view_new (mts->model); e_meeting_list_view_column_set_visible (mts->list_view, E_MEETING_STORE_ROLE_COL, FALSE); e_meeting_list_view_column_set_visible (mts->list_view, E_MEETING_STORE_RSVP_COL, FALSE); e_meeting_list_view_column_set_visible (mts->list_view, E_MEETING_STORE_STATUS_COL, FALSE); e_meeting_list_view_column_set_visible (mts->list_view, E_MEETING_STORE_TYPE_COL, FALSE); gtk_widget_show (GTK_WIDGET (mts->list_view)); sw = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_IN); gtk_widget_set_child_visible ( gtk_scrolled_window_get_vscrollbar ( GTK_SCROLLED_WINDOW (sw)), FALSE); gtk_widget_show (sw); gtk_container_add (GTK_CONTAINER (sw), GTK_WIDGET (mts->list_view)); #if 0 /* FIXME: do we need sorting here */ g_signal_connect ( real_table->sort_info, "sort_info_changed", G_CALLBACK (sort_info_changed_cb), mts); #endif gtk_box_pack_start (GTK_BOX (mts->attendees_vbox), GTK_WIDGET (sw), TRUE, TRUE, 6); /* The free/busy information */ mts->display_top = gnome_canvas_new (); gtk_widget_set_size_request (mts->display_top, -1, mts->row_height * 3); 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, 0, 0); gtk_widget_show (mts->display_top); g_signal_connect ( mts->display_top, "realize", G_CALLBACK (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, 0, 0); gtk_widget_show (mts->display_main); g_signal_connect ( mts->display_main, "realize", G_CALLBACK (e_meeting_time_selector_on_canvas_realized), mts); g_signal_connect ( mts->display_main, "size_allocate", G_CALLBACK (e_meeting_time_selector_on_canvas_size_allocate), mts); g_signal_connect ( mts->display_main, "scroll-event", G_CALLBACK (e_meeting_time_selector_on_canvas_scroll_event), mts); /* used for displaying extended free/busy (XFB) display when hovering * over a busy period which carries XFB information */ g_signal_connect ( mts->display_main, "query-tooltip", G_CALLBACK (e_meeting_time_selector_on_canvas_query_tooltip), mts); g_object_set ( G_OBJECT (mts->display_main), "has-tooltip", TRUE, NULL); scrollable = GTK_SCROLLABLE (mts->display_main); adjustment = gtk_scrollable_get_vadjustment (scrollable); gtk_scrolled_window_set_vadjustment ( GTK_SCROLLED_WINDOW (sw), adjustment); adjustment = gtk_scrollable_get_hadjustment (scrollable); mts->hscrollbar = gtk_scrollbar_new ( GTK_ORIENTATION_HORIZONTAL, adjustment); gtk_adjustment_set_step_increment (adjustment, mts->day_width); gtk_table_attach ( GTK_TABLE (mts), mts->hscrollbar, 1, 4, 2, 3, GTK_EXPAND | GTK_FILL, 0, 0, 0); gtk_widget_show (mts->hscrollbar); adjustment = gtk_scrollable_get_vadjustment (scrollable); mts->vscrollbar = gtk_scrollbar_new ( GTK_ORIENTATION_VERTICAL, adjustment); gtk_adjustment_set_step_increment (adjustment, mts->row_height); 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. */ mts->item_top = 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. */ mts->item_main = 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_box_new (GTK_ORIENTATION_HORIZONTAL, 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_FREE_BUSY_TENTATIVE]); e_meeting_time_selector_add_key_color (mts, hbox, _("Busy"), &mts->busy_colors[E_MEETING_FREE_BUSY_BUSY]); e_meeting_time_selector_add_key_color (mts, hbox, _("Out of Office"), &mts->busy_colors[E_MEETING_FREE_BUSY_OUT_OF_OFFICE]); e_meeting_time_selector_add_key_color ( mts, hbox, _("No Information"), NULL); separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); gtk_table_attach ( GTK_TABLE (mts), separator, 0, 5, 4, 5, GTK_FILL, 0, 6, 6); gtk_widget_show (separator); /* Create the Invite Others & Options buttons on the left. */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); gtk_table_attach ( GTK_TABLE (mts), hbox, 0, 1, 3, 4, GTK_FILL, 0, 0, 0); gtk_widget_show (hbox); mts->add_attendees_button = gtk_button_new_with_mnemonic (_("Atte_ndees...")); gtk_button_set_image ( GTK_BUTTON (mts->add_attendees_button), gtk_image_new_from_stock ( GTK_STOCK_JUMP_TO, GTK_ICON_SIZE_BUTTON)); gtk_box_pack_start (GTK_BOX (hbox), mts->add_attendees_button, TRUE, TRUE, 6); gtk_widget_show (mts->add_attendees_button); g_signal_connect ( mts->add_attendees_button, "clicked", G_CALLBACK (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, 6); gtk_widget_show (mts->options_button); g_signal_connect ( mts->options_button, "clicked", G_CALLBACK (e_meeting_time_selector_on_options_button_clicked), mts); child_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2); gtk_container_add (GTK_CONTAINER (mts->options_button), child_hbox); gtk_widget_show (child_hbox); label = gtk_label_new_with_mnemonic (_("O_ptions")); accel_key = gtk_label_get_mnemonic_keyval (GTK_LABEL (label)); gtk_box_pack_start (GTK_BOX (child_hbox), label, TRUE, TRUE, 6); 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_NONE); gtk_box_pack_start (GTK_BOX (child_hbox), arrow, FALSE, FALSE, 6); 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); menuitem = gtk_check_menu_item_new_with_label (""); child = gtk_bin_get_child (GTK_BIN (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("Show _only working hours")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->options_menu), menuitem); gtk_check_menu_item_set_active ( GTK_CHECK_MENU_ITEM (menuitem), mts->working_hours_only); g_signal_connect ( menuitem, "toggled", G_CALLBACK (e_meeting_time_selector_on_working_hours_toggled), mts); gtk_widget_show (menuitem); menuitem = gtk_check_menu_item_new_with_label (""); child = gtk_bin_get_child (GTK_BIN (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("Show _zoomed out")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->options_menu), menuitem); gtk_check_menu_item_set_active ( GTK_CHECK_MENU_ITEM (menuitem), mts->zoomed_out); g_signal_connect ( menuitem, "toggled", G_CALLBACK (e_meeting_time_selector_on_zoomed_out_toggled), mts); gtk_widget_show (menuitem); menuitem = gtk_menu_item_new (); gtk_menu_shell_append (GTK_MENU_SHELL (mts->options_menu), menuitem); gtk_widget_set_sensitive (menuitem, FALSE); gtk_widget_show (menuitem); menuitem = gtk_menu_item_new_with_label (""); child = gtk_bin_get_child (GTK_BIN (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("_Update free/busy")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->options_menu), menuitem); g_signal_connect ( menuitem, "activate", G_CALLBACK (e_meeting_time_selector_on_update_free_busy), mts); gtk_widget_show (menuitem); /* Create the 3 AutoPick buttons on the left. */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_table_attach ( GTK_TABLE (mts), hbox, 0, 1, 5, 6, GTK_FILL, 0, 0, 0); gtk_widget_show (hbox); mts->autopick_down_button = gtk_button_new_with_label (""); child = gtk_bin_get_child (GTK_BIN (mts->autopick_down_button)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("_<<")); accel_key = gtk_label_get_mnemonic_keyval (GTK_LABEL (child)); gtk_widget_add_accelerator ( mts->autopick_down_button, "clicked", mts->accel_group, accel_key, GDK_MOD1_MASK | GDK_SHIFT_MASK, 0); gtk_box_pack_start (GTK_BOX (hbox), mts->autopick_down_button, TRUE, TRUE, 6); gtk_widget_show (mts->autopick_down_button); g_signal_connect ( mts->autopick_down_button, "clicked", G_CALLBACK (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, 6); gtk_widget_show (mts->autopick_button); child_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2); gtk_container_add (GTK_CONTAINER (mts->autopick_button), child_hbox); gtk_widget_show (child_hbox); label = gtk_label_new (""); gtk_label_set_text_with_mnemonic (GTK_LABEL (label), _("_Autopick")); accel_key = gtk_label_get_mnemonic_keyval (GTK_LABEL (label)); gtk_box_pack_start (GTK_BOX (child_hbox), label, TRUE, TRUE, 6); gtk_widget_show (label); gtk_widget_add_accelerator ( mts->autopick_button, "clicked", mts->accel_group, accel_key, GDK_MOD1_MASK, 0); g_signal_connect ( mts->autopick_button, "clicked", G_CALLBACK (e_meeting_time_selector_on_autopick_button_clicked), mts); arrow = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_NONE); gtk_box_pack_start (GTK_BOX (child_hbox), arrow, FALSE, FALSE, 6); gtk_widget_show (arrow); mts->autopick_up_button = gtk_button_new_with_label (""); child = gtk_bin_get_child (GTK_BIN (mts->autopick_up_button)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _(">_>")); accel_key = gtk_label_get_mnemonic_keyval (GTK_LABEL (child)); gtk_widget_add_accelerator ( mts->autopick_up_button, "clicked", mts->accel_group, accel_key, GDK_MOD1_MASK | GDK_SHIFT_MASK, 0); gtk_box_pack_start (GTK_BOX (hbox), mts->autopick_up_button, TRUE, TRUE, 6); gtk_widget_show (mts->autopick_up_button); g_signal_connect ( mts->autopick_up_button, "clicked", G_CALLBACK (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); menuitem = gtk_radio_menu_item_new_with_label (NULL, ""); mts->autopick_all_item = menuitem; child = gtk_bin_get_child (GTK_BIN (menuitem)); group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("_All people and resources")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->autopick_menu), menuitem); g_signal_connect ( menuitem, "toggled", G_CALLBACK (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; child = gtk_bin_get_child (GTK_BIN (menuitem)); group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("All _people and one resource")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->autopick_menu), menuitem); g_signal_connect ( menuitem, "toggled", G_CALLBACK (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; child = gtk_bin_get_child (GTK_BIN (menuitem)); group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("_Required people")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->autopick_menu), menuitem); g_signal_connect ( menuitem, "activate", G_CALLBACK (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; child = gtk_bin_get_child (GTK_BIN (menuitem)); gtk_label_set_text_with_mnemonic (GTK_LABEL (child), _("Required people and _one resource")); gtk_menu_shell_append (GTK_MENU_SHELL (mts->autopick_menu), menuitem); g_signal_connect ( menuitem, "activate", G_CALLBACK (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.0, 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); mts->start_date_edit = e_date_edit_new (); gtk_label_set_mnemonic_widget (GTK_LABEL (label), mts->start_date_edit); a11y_label = gtk_widget_get_accessible (label); a11y_date_edit = gtk_widget_get_accessible (mts->start_date_edit); if (a11y_label != NULL && a11y_date_edit != NULL) { atk_object_add_relationship ( a11y_date_edit, ATK_RELATION_LABELLED_BY, a11y_label); } e_date_edit_set_show_time (E_DATE_EDIT (mts->start_date_edit), TRUE); 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); g_signal_connect ( mts->start_date_edit, "changed", G_CALLBACK (e_meeting_time_selector_on_start_time_changed), mts); label = gtk_label_new_with_mnemonic (_("_Start time:")); gtk_label_set_mnemonic_widget (GTK_LABEL (label), (mts->start_date_edit)); 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->end_date_edit = e_date_edit_new (); gtk_label_set_mnemonic_widget (GTK_LABEL (label), mts->end_date_edit); a11y_label = gtk_widget_get_accessible (label); a11y_date_edit = gtk_widget_get_accessible (mts->end_date_edit); if (a11y_label != NULL && a11y_date_edit != NULL) { atk_object_add_relationship ( a11y_date_edit, ATK_RELATION_LABELLED_BY, a11y_label); } e_date_edit_set_show_time (E_DATE_EDIT (mts->end_date_edit), TRUE); 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); g_signal_connect ( mts->end_date_edit, "changed", G_CALLBACK (e_meeting_time_selector_on_end_time_changed), mts); label = gtk_label_new_with_mnemonic (_("_End time:")); gtk_label_set_mnemonic_widget (GTK_LABEL (label), (mts->end_date_edit)); 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); gtk_table_set_col_spacing (GTK_TABLE (mts), 0, 4); gtk_table_set_row_spacing (GTK_TABLE (mts), 4, 12); /* Allocate the colors. */ e_meeting_time_selector_alloc_named_color (mts, "snow", &mts->bg_color); e_meeting_time_selector_alloc_named_color (mts, "snow3", &mts->all_attendees_bg_color); e_meeting_time_selector_alloc_named_color (mts, "black", &mts->grid_color); e_meeting_time_selector_alloc_named_color (mts, "white", &mts->grid_shadow_color); e_meeting_time_selector_alloc_named_color (mts, "gray50", &mts->grid_unused_color); e_meeting_time_selector_alloc_named_color (mts, "white", &mts->attendee_list_bg_color); e_meeting_time_selector_alloc_named_color (mts, "snow4", &mts->meeting_time_bg_color); e_meeting_time_selector_alloc_named_color (mts, "snow", &mts->busy_colors[E_MEETING_FREE_BUSY_FREE]); e_meeting_time_selector_alloc_named_color (mts, "#a5d3ef", &mts->busy_colors[E_MEETING_FREE_BUSY_TENTATIVE]); e_meeting_time_selector_alloc_named_color (mts, "blue", &mts->busy_colors[E_MEETING_FREE_BUSY_BUSY]); e_meeting_time_selector_alloc_named_color (mts, "#ce6194", &mts->busy_colors[E_MEETING_FREE_BUSY_OUT_OF_OFFICE]); /* Connect handlers to the adjustments scroll the other items. */ scrollable = GTK_SCROLLABLE (mts->display_main); adjustment = gtk_scrollable_get_hadjustment (scrollable); g_signal_connect ( adjustment, "value_changed", G_CALLBACK (e_meeting_time_selector_hadjustment_changed), mts); adjustment = gtk_scrollable_get_vadjustment (scrollable); g_signal_connect ( adjustment, "value_changed", G_CALLBACK (e_meeting_time_selector_vadjustment_changed), mts); g_signal_connect ( adjustment, "changed", G_CALLBACK (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); g_signal_emit (mts, signals[CHANGED], 0); } /* This adds a color to the color key beneath the main display. If color is * NULL, it displays the No Info pattern 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_box_new (GTK_ORIENTATION_HORIZONTAL, 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); g_object_set_data (G_OBJECT (darea), "data", mts); gtk_widget_set_size_request (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, 6); gtk_widget_show (label); g_signal_connect ( darea, "draw", G_CALLBACK (e_meeting_time_selector_draw_key_color), color); } static gint e_meeting_time_selector_draw_key_color (GtkWidget *darea, cairo_t *cr, GdkColor *color) { EMeetingTimeSelector * mts; GtkAllocation allocation; GtkStyleContext *style_context; mts = g_object_get_data (G_OBJECT (darea), "data"); style_context = gtk_widget_get_style_context (darea); gtk_widget_get_allocation (darea, &allocation); gtk_render_frame ( style_context, cr, (gdouble) 0, (gdouble) 0, (gdouble) allocation.width, (gdouble) allocation.height); if (color) { gdk_cairo_set_source_color (cr, color); } else { cairo_set_source (cr, mts->no_info_pattern); } cairo_rectangle ( cr, 1, 1, allocation.width - 2, allocation.height - 2); cairo_fill (cr); return TRUE; } static void e_meeting_time_selector_alloc_named_color (EMeetingTimeSelector *mts, const gchar *name, GdkColor *c) { g_return_if_fail (name != NULL); g_return_if_fail (c != NULL); if (!gdk_color_parse (name, c)) g_warning ("Failed to parse 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 (E_IS_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 (E_IS_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 (EMeetingStore *ems) { GtkWidget *mts; mts = g_object_new (E_TYPE_MEETING_TIME_SELECTOR, NULL); e_meeting_time_selector_construct (E_MEETING_TIME_SELECTOR (mts), ems); return mts; } gboolean e_meeting_time_selector_get_use_24_hour_format (EMeetingTimeSelector *mts) { g_return_val_if_fail (E_IS_MEETING_TIME_SELECTOR (mts), FALSE); return mts->priv->use_24_hour_format; } void e_meeting_time_selector_set_use_24_hour_format (EMeetingTimeSelector *mts, gboolean use_24_hour_format) { g_return_if_fail (E_IS_MEETING_TIME_SELECTOR (mts)); if (mts->priv->use_24_hour_format == use_24_hour_format) return; mts->priv->use_24_hour_format = use_24_hour_format; g_object_notify (G_OBJECT (mts), "use-24-hour-format"); } static cairo_pattern_t * e_meeting_time_selector_create_no_info_pattern (EMeetingTimeSelector *mts) { cairo_surface_t *surface; cairo_pattern_t *pattern; GdkColor color; cairo_t *cr; surface = gdk_window_create_similar_surface ( gtk_widget_get_window (GTK_WIDGET (mts)), CAIRO_CONTENT_COLOR, 8, 8); cr = cairo_create (surface); gdk_color_parse ("white", &color); gdk_cairo_set_source_color (cr, &color); cairo_paint (cr); gdk_cairo_set_source_color (cr, &mts->grid_color); cairo_set_line_width (cr, 1.0); cairo_move_to (cr, -1, 5); cairo_line_to (cr, 9, -5); cairo_move_to (cr, -1, 13); cairo_line_to (cr, 9, 3); cairo_stroke (cr); cairo_destroy (cr); pattern = cairo_pattern_create_for_surface (surface); cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT); cairo_surface_destroy (surface); return pattern; } 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->no_info_pattern = e_meeting_time_selector_create_no_info_pattern (mts); } static void e_meeting_time_selector_unrealize (GtkWidget *widget) { EMeetingTimeSelector *mts; mts = E_MEETING_TIME_SELECTOR (widget); cairo_pattern_destroy (mts->no_info_pattern); mts->no_info_pattern = NULL; if (GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->unrealize) (*GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->unrealize)(widget); } static gint get_cell_height (GtkTreeView *tree) { GtkTreeViewColumn *column; gint height = -1; column = gtk_tree_view_get_column (tree, 0); gtk_tree_view_column_cell_get_size ( column, NULL, NULL, NULL, NULL, &height); return height; } static gboolean style_change_idle_func (EMeetingTimeSelector *mts) { EMeetingTime saved_time; GtkAdjustment *adjustment; GtkWidget *widget; gint hour, max_hour_width; /*int maxheight; */ PangoFontDescription *font_desc; PangoContext *pango_context; PangoFontMetrics *font_metrics; PangoLayout *layout; /* Set up Pango prerequisites */ widget = GTK_WIDGET (mts); font_desc = gtk_widget_get_style (widget)->font_desc; pango_context = gtk_widget_get_pango_context (widget); font_metrics = pango_context_get_metrics ( pango_context, font_desc, pango_context_get_language (pango_context)); layout = pango_layout_new (pango_context); /* Calculate the widths of the hour strings in the style's font. */ max_hour_width = 0; for (hour = 0; hour < 24; hour++) { if (e_meeting_time_selector_get_use_24_hour_format (mts)) pango_layout_set_text (layout, EMeetingTimeSelectorHours[hour], -1); else pango_layout_set_text (layout, EMeetingTimeSelectorHours12[hour], -1); pango_layout_get_pixel_size (layout, &mts->hour_widths[hour], NULL); max_hour_width = MAX (max_hour_width, mts->hour_widths[hour]); } /* add also some padding for lines so it fits better */ mts->row_height = get_cell_height (GTK_TREE_VIEW (mts->list_view)) + 2; mts->col_width = max_hour_width + 6; 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_size_request (mts->display_top, -1, mts->row_height * 3 + 4); /* * FIXME: I can't find a way to get the treeview header heights * other than the below but it isn't nice to realize that widget here * * gtk_widget_realize (mts->list_view); gdk_window_get_position ( gtk_tree_view_get_bin_window (GTK_TREE_VIEW (mts->list_view)), NULL, &maxheight); gtk_widget_set_size_request (mts->attendees_vbox_spacer, 1, mts->row_height * 3 - maxheight); * */ gtk_widget_set_size_request (mts->attendees_vbox_spacer, 1, mts->row_height * 2 - 6); widget = mts->display_main; adjustment = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE (widget)); gtk_adjustment_set_step_increment (adjustment, mts->day_width); adjustment = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (widget)); gtk_adjustment_set_step_increment (adjustment, mts->row_height); g_object_unref (layout); pango_font_metrics_unref (font_metrics); mts->style_change_idle_id = 0; return FALSE; } static void e_meeting_time_selector_style_set (GtkWidget *widget, GtkStyle *previous_style) { EMeetingTimeSelector *mts = E_MEETING_TIME_SELECTOR (widget); 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); if (!mts->style_change_idle_id) mts->style_change_idle_id = g_idle_add ( (GSourceFunc) style_change_idle_func, widget); } /* This draws a shadow around the top display and main display. */ static gint e_meeting_time_selector_draw (GtkWidget *widget, cairo_t *cr) { EMeetingTimeSelector *mts; mts = E_MEETING_TIME_SELECTOR (widget); e_meeting_time_selector_draw_shadow (mts, cr); if (GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->draw) (*GTK_WIDGET_CLASS (e_meeting_time_selector_parent_class)->draw)(widget, cr); return FALSE; } static void e_meeting_time_selector_draw_shadow (EMeetingTimeSelector *mts, cairo_t *cr) { GtkAllocation allocation; GtkStyleContext *style_context; style_context = gtk_widget_get_style_context (GTK_WIDGET (mts)); /* Draw the shadow around the graphical displays. */ gtk_widget_get_allocation (mts->display_top, &allocation); cairo_save (cr); gtk_render_frame ( style_context, cr, (gdouble) allocation.x - 2, (gdouble) allocation.y - 2, (gdouble) allocation.width + 4, (gdouble) allocation.height + allocation.height + 4); cairo_restore (cr); } /* When the main canvas scrolls, we scroll the other canvases. */ static void e_meeting_time_selector_hadjustment_changed (GtkAdjustment *adjustment, EMeetingTimeSelector *mts) { GtkAdjustment *hadjustment; GtkScrollable *scrollable; gdouble value; scrollable = GTK_SCROLLABLE (mts->display_top); hadjustment = gtk_scrollable_get_hadjustment (scrollable); value = gtk_adjustment_get_value (adjustment); gtk_adjustment_set_value (hadjustment, value); } static void e_meeting_time_selector_vadjustment_changed (GtkAdjustment *adjustment, EMeetingTimeSelector *mts) { GtkAdjustment *vadjustment; GtkScrollable *scrollable; gdouble value; scrollable = GTK_SCROLLABLE (mts->list_view); vadjustment = gtk_scrollable_get_vadjustment (scrollable); value = gtk_adjustment_get_value (adjustment); gtk_adjustment_set_value (vadjustment, value); } 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_get_year (&mts->meeting_start_time.date); *start_month = g_date_get_month (&mts->meeting_start_time.date); *start_day = g_date_get_day (&mts->meeting_start_time.date); *start_hour = mts->meeting_start_time.hour; *start_minute = mts->meeting_start_time.minute; *end_year = g_date_get_year (&mts->meeting_end_time.date); *end_month = g_date_get_month (&mts->meeting_end_time.date); *end_day = g_date_get_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 (E_IS_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 EDateEdit widgets. */ e_meeting_time_selector_update_start_date_edit (mts); e_meeting_time_selector_update_end_date_edit (mts); g_signal_emit (mts, signals[CHANGED], 0); return TRUE; } void e_meeting_time_selector_set_all_day (EMeetingTimeSelector *mts, gboolean all_day) { EMeetingTime saved_time; mts->all_day = all_day; e_date_edit_set_show_time ( E_DATE_EDIT (mts->start_date_edit), !all_day); e_date_edit_set_show_time ( E_DATE_EDIT (mts->end_date_edit), !all_day); 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_only (EMeetingTimeSelector *mts, gboolean working_hours_only) { EMeetingTime saved_time; g_return_if_fail (E_IS_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) { EMeetingTime saved_time; g_return_if_fail (E_IS_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; /* Make sure we always show atleast an hour */ if (day_start_hour * 60 + day_start_minute + 60 < day_end_hour * 60 + day_end_minute) { mts->day_end_hour = day_end_hour; mts->day_end_minute = day_end_minute; } else { mts->day_end_hour = day_start_hour + 1; mts->day_end_minute = day_start_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) { EMeetingTime saved_time; g_return_if_fail (E_IS_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); } static gboolean e_meeting_time_selector_refresh_cb (gpointer data) { EMeetingTimeSelector *mts = data; if (e_meeting_store_get_num_queries (mts->model) == 0) { GdkCursor *cursor; GdkWindow *window; cursor = gdk_cursor_new (GDK_LEFT_PTR); window = gtk_widget_get_window (GTK_WIDGET (mts)); if (window) gdk_window_set_cursor (window, cursor); g_object_unref (cursor); mts->last_cursor_set = GDK_LEFT_PTR; e_meeting_time_selector_item_set_normal_cursor (E_MEETING_TIME_SELECTOR_ITEM (mts->item_top)); e_meeting_time_selector_item_set_normal_cursor (E_MEETING_TIME_SELECTOR_ITEM (mts->item_main)); } if (mts->display_top != NULL) gtk_widget_queue_draw (mts->display_top); if (mts->display_main != NULL) gtk_widget_queue_draw (mts->display_main); g_object_unref (mts); return FALSE; } void e_meeting_time_selector_refresh_free_busy (EMeetingTimeSelector *mts, gint row, gboolean all) { EMeetingTime start, end; /* nothing to refresh, lets not leak a busy cursor */ if (e_meeting_store_count_actual_attendees (mts->model) <= 0) return; start = mts->meeting_start_time; g_date_subtract_days (&start.date, E_MEETING_TIME_SELECTOR_FB_DAYS_BEFORE); start.hour = 0; start.minute = 0; end = mts->meeting_end_time; g_date_add_days (&end.date, E_MEETING_TIME_SELECTOR_FB_DAYS_AFTER); end.hour = 0; end.minute = 0; /* XXX This function is called during schedule page initialization * before the meeting time selector is realized, meaning it has * no GdkWindow yet. This avoids a runtime warning. */ if (gtk_widget_get_realized (GTK_WIDGET (mts))) { GdkCursor *cursor; GdkWindow *window; /* Set the cursor to Busy. We need to reset it to * normal once the free busy queries are complete. */ cursor = gdk_cursor_new (GDK_WATCH); window = gtk_widget_get_window (GTK_WIDGET (mts)); gdk_window_set_cursor (window, cursor); g_object_unref (cursor); mts->last_cursor_set = GDK_WATCH; } /* Ref ourselves in case we are called back after destruction, * we can do this because we will get a call back even after * an error */ /* FIXME We should really have a mechanism to unqueue the * notification */ if (all) { gint i; for (i = 0; i < e_meeting_store_count_actual_attendees (mts->model); i++) g_object_ref (mts); } else { g_object_ref (mts); } if (all) e_meeting_store_refresh_all_busy_periods ( mts->model, &start, &end, e_meeting_time_selector_refresh_cb, mts); else e_meeting_store_refresh_busy_periods ( mts->model, row, &start, &end, e_meeting_time_selector_refresh_cb, mts); } EMeetingTimeSelectorAutopickOption e_meeting_time_selector_get_autopick_option (EMeetingTimeSelector *mts) { GtkWidget *widget; widget = mts->autopick_all_item; if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget))) return E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_RESOURCES; widget = mts->autopick_all_people_one_resource_item; if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget))) return E_MEETING_TIME_SELECTOR_ALL_PEOPLE_AND_ONE_RESOURCE; widget = mts->autopick_required_people_item; if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget))) 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 (E_IS_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; } } void e_meeting_time_selector_set_read_only (EMeetingTimeSelector *mts, gboolean read_only) { g_return_if_fail (E_IS_MEETING_TIME_SELECTOR (mts)); gtk_widget_set_sensitive (GTK_WIDGET (mts->list_view), !read_only); gtk_widget_set_sensitive (mts->display_main, !read_only); gtk_widget_set_sensitive (mts->add_attendees_button, !read_only); gtk_widget_set_sensitive (mts->autopick_down_button, !read_only); gtk_widget_set_sensitive (mts->autopick_button, !read_only); gtk_widget_set_sensitive (mts->autopick_up_button, !read_only); gtk_widget_set_sensitive (mts->start_date_edit, !read_only); gtk_widget_set_sensitive (mts->end_date_edit, !read_only); } /* * 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 (E_IS_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); /* These are just for debugging so don't need i18n. */ g_date_strftime ( buffer, sizeof (buffer), "%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, sizeof (buffer), "%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 EMeetingTimein a string and returns it. * Note that it uses a static buffer. */ gchar * e_meeting_time_selector_dump_time (EMeetingTime *mtstime) { static gchar buffer[128]; gchar buffer2[128]; /* This is just for debugging so doesn't need i18n. */ g_date_strftime ( buffer, sizeof (buffer), "%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]; /* This is just for debugging so doesn't need i18n. */ g_date_strftime (buffer, sizeof (buffer), "%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) { e_meeting_list_view_invite_others_dialog (mts->list_view); } 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, gboolean *push_in, gpointer user_data) { EMeetingTimeSelector *mts; GtkRequisition menu_requisition; GtkAllocation allocation; GtkWidget *widget; GdkWindow *window; gint max_x, max_y; mts = E_MEETING_TIME_SELECTOR (user_data); /* Calculate our preferred position. */ widget = mts->options_button; window = gtk_widget_get_window (widget); gdk_window_get_origin (window, x, y); gtk_widget_get_allocation (widget, &allocation); *x += allocation.x; *y += allocation.y + allocation.height - 2; /* Now make sure we are on the screen. */ gtk_widget_get_preferred_size (mts->options_menu, &menu_requisition, NULL); 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_get_visible (mts->options_menu)) gtk_menu_popdown (GTK_MENU (mts->options_menu)); e_meeting_time_selector_refresh_free_busy (mts, 0, TRUE); } 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, gboolean *push_in, gpointer user_data) { EMeetingTimeSelector *mts; GtkRequisition menu_requisition; GtkAllocation allocation; GtkWidget *widget; GdkWindow *window; gint max_x, max_y; mts = E_MEETING_TIME_SELECTOR (user_data); /* Calculate our preferred position. */ widget = mts->autopick_button; window = gtk_widget_get_window (widget); gdk_window_get_origin (window, x, y); gtk_widget_get_allocation (widget, &allocation); *x += allocation.x; *y += allocation.y + allocation.height - 2; /* Now make sure we are on the screen. */ gtk_widget_get_preferred_size (mts->autopick_menu, &menu_requisition, NULL); 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_get_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) { EMeetingTime start_time, end_time, *resource_free; EMeetingAttendee *attendee; EMeetingFreeBusyPeriod *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 < e_meeting_store_count_actual_attendees (mts->model); row++) { attendee = e_meeting_store_find_attendee_at_row (mts->model, row); /* Skip optional people if they don't matter. */ if (skip_optional && e_meeting_attendee_get_atype (attendee) == E_MEETING_ATTENDEE_OPTIONAL_PERSON) continue; period = e_meeting_time_selector_find_time_clash (mts, attendee, &start_time, &end_time); if (need_one_resource && e_meeting_attendee_get_atype (attendee) == E_MEETING_ATTENDEE_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_compare_times (resource_free, &period->end) > 0) resource_free = &period->end; } else { if (!resource_free || e_meeting_time_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 EDateEdit widgets. */ e_meeting_time_selector_update_start_date_edit (mts); e_meeting_time_selector_update_end_date_edit (mts); g_signal_emit (mts, signals[CHANGED], 0); 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 (EMeetingTime *start, EMeetingTime *end, gint *days, gint *hours, gint *minutes) { *days = g_date_get_julian (&end->date) - g_date_get_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, EMeetingTime *start_time, EMeetingTime *end_time, gint days, gint hours, gint mins) { gint minutes_shown; gboolean set_to_start_of_working_day = FALSE; if (!mts->all_day) { if (mts->zoomed_out) { start_time->hour++; start_time->minute = 0; } else { start_time->minute += 30; start_time->minute -= start_time->minute % 30; } } else { g_date_add_days (&start_time->date, 1); start_time->hour = 0; start_time->minute = 0; } 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, EMeetingTime *start_time, EMeetingTime *end_time, gint days, gint hours, gint mins) { gint new_hour, minutes_shown; gboolean set_to_end_of_working_day = FALSE; if (!mts->all_day) { 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; } else { g_date_subtract_days (&start_time->date, 1); start_time->hour = 0; start_time->minute = 0; } *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 (EMeetingTime *mtstime, gint days, gint hours, gint minutes) { gint new_hours, new_minutes; /* We have to handle negative values for hous and minutes here, since * EMeetingTimeuses 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 EMeetingFreeBusyPeriod * e_meeting_time_selector_find_time_clash (EMeetingTimeSelector *mts, EMeetingAttendee *attendee, EMeetingTime *start_time, EMeetingTime *end_time) { EMeetingFreeBusyPeriod *period; const GArray *busy_periods; gint period_num; busy_periods = e_meeting_attendee_get_busy_periods (attendee); period_num = e_meeting_attendee_find_first_busy_period (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 < busy_periods->len) { period = &g_array_index (busy_periods, EMeetingFreeBusyPeriod, 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_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_compare_times (&period->end, start_time) > 0) return period; period_num++; } return NULL; } static void e_meeting_time_selector_on_zoomed_out_toggled (GtkCheckMenuItem *menuitem, EMeetingTimeSelector *mts) { gboolean active; /* Make sure the menu pops down, which doesn't happen by default if * keyboard accelerators are used. */ if (gtk_widget_get_visible (mts->options_menu)) gtk_menu_popdown (GTK_MENU (mts->options_menu)); active = gtk_check_menu_item_get_active (menuitem); e_meeting_time_selector_set_zoomed_out (mts, active); e_meeting_time_selector_ensure_meeting_time_shown (mts); } static void e_meeting_time_selector_on_working_hours_toggled (GtkCheckMenuItem *menuitem, EMeetingTimeSelector *mts) { gboolean active; /* Make sure the menu pops down, which doesn't happen by default if * keyboard accelerators are used. */ if (gtk_widget_get_visible (mts->options_menu)) gtk_menu_popdown (GTK_MENU (mts->options_menu)); active = gtk_check_menu_item_get_active (menuitem); e_meeting_time_selector_set_working_hours_only (mts, active); e_meeting_time_selector_ensure_meeting_time_shown (mts); } /* 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, EMeetingTime *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, EMeetingTime *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) { /* An array of dates, one for each month in the year 2000. They must * all be Sundays. */ static const gint days[12] = { 23, 20, 19, 23, 21, 18, 23, 20, 17, 22, 19, 24 }; GDate date; gint max_date_width, longest_weekday_width, longest_month_width, width; gint day, longest_weekday, month, longest_month; gchar buffer[128], *str; const gchar *name; PangoContext *pango_context; PangoLayout *layout; struct tm tm_time; /* Set up Pango prerequisites */ pango_context = gtk_widget_get_pango_context (GTK_WIDGET (mts)); layout = pango_layout_new (pango_context); /* Calculate the maximum date width we can fit into the display. */ max_date_width = mts->day_width - 2; /* Find the biggest full weekday name. We start on a particular * Monday and go through seven days. */ g_date_clear (&date, 1); g_date_set_dmy (&date, 3, 1, 2000); /* Monday 3rd Jan 2000. */ longest_weekday_width = 0; longest_weekday = G_DATE_MONDAY; for (day = G_DATE_MONDAY; day <= G_DATE_SUNDAY; day++) { name = e_get_weekday_name (day, FALSE); pango_layout_set_text (layout, name, -1); pango_layout_get_pixel_size (layout, &width, NULL); if (width > longest_weekday_width) { longest_weekday = day; longest_weekday_width = width; } } /* Now find the biggest month name. */ longest_month_width = 0; longest_month = G_DATE_JANUARY; for (month = G_DATE_JANUARY; month <= G_DATE_DECEMBER; month++) { name = e_get_month_name (month, FALSE); pango_layout_set_text (layout, name, -1); pango_layout_get_pixel_size (layout, &width, NULL); if (width > longest_month_width) { longest_month = month; longest_month_width = width; } } /* Now try it with abbreviated weekday names. */ longest_weekday_width = 0; longest_weekday = G_DATE_MONDAY; for (day = G_DATE_MONDAY; day <= G_DATE_SUNDAY; day++) { name = e_get_weekday_name (day, TRUE); pango_layout_set_text (layout, name, -1); pango_layout_get_pixel_size (layout, &width, NULL); if (width > longest_weekday_width) { longest_weekday = day; longest_weekday_width = width; } } g_date_set_dmy ( &date, days[longest_month - 1] + longest_weekday, longest_month, 2000); g_date_to_struct_tm (&date, &tm_time); str = e_datetime_format_format_tm ("calendar", "table", DTFormatKindDate, &tm_time); g_return_if_fail (str != NULL); if (!e_datetime_format_includes_day_name ("calendar", "table", DTFormatKindDate)) { gchar *tmp; g_date_strftime (buffer, sizeof (buffer), "%a", &date); tmp = str; str = g_strconcat (buffer, " ", str, NULL); g_free (tmp); } #if 0 g_print ( "longest_month: %i longest_weekday: %i date: %s\n", longest_month, longest_weekday, str); #endif pango_layout_set_text (layout, str, -1); pango_layout_get_pixel_size (layout, &width, NULL); if (width < max_date_width) mts->date_format = E_MEETING_TIME_SELECTOR_DATE_ABBREVIATED_DAY; else mts->date_format = E_MEETING_TIME_SELECTOR_DATE_SHORT; g_object_unref (layout); g_free (str); } /* 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) { GdkWindow *window; window = gtk_layout_get_bin_window (GTK_LAYOUT (widget)); gdk_window_set_background_pattern (window, NULL); } /* 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; EMeetingTime mtstime; gint hour = 0, minute = 0; time_t newtime; /* Date */ newtime = e_date_edit_get_time (E_DATE_EDIT (mts->start_date_edit)); g_date_clear (&mtstime.date, 1); g_date_set_time_t (&mtstime.date, newtime); /* Time */ e_date_edit_get_time_of_day (E_DATE_EDIT (mts->start_date_edit), &hour, &minute); mtstime.hour = hour; mtstime.minute = minute; /* If the time hasn't changed, just return. */ if (e_meeting_time_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); g_signal_emit (mts, signals[CHANGED], 0); } /* 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) { EMeetingTime mtstime; gint hour = 0, minute = 0; time_t newtime; /* Date */ newtime = e_date_edit_get_time (E_DATE_EDIT (mts->end_date_edit)); g_date_clear (&mtstime.date, 1); g_date_set_time_t (&mtstime.date, newtime); if (mts->all_day) g_date_add_days (&mtstime.date, 1); /* Time */ e_date_edit_get_time_of_day (E_DATE_EDIT (mts->end_date_edit), &hour, &minute); mtstime.hour = hour; mtstime.minute = minute; /* If the time hasn't changed, just return. */ if (e_meeting_time_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_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; if (mts->all_day) g_date_subtract_days (&mts->meeting_start_time.date, 1); 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); g_signal_emit (mts, signals[CHANGED], 0); } /* 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) { EDateEdit *start_edit, *end_edit; gint low_hour, high_hour; start_edit = E_DATE_EDIT (mts->start_date_edit); end_edit = E_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; } e_date_edit_set_time_popup_range (start_edit, low_hour, high_hour); e_date_edit_set_time_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 gboolean e_meeting_time_selector_on_canvas_scroll_event (GtkWidget *widget, GdkEventScroll *event, EMeetingTimeSelector *mts) { gboolean return_val = FALSE; /* escalate to the list view's parent, which is a scrolled window */ g_signal_emit_by_name (gtk_widget_get_parent (GTK_WIDGET (mts->list_view)), "scroll-event", event, &return_val); return return_val; } /* Sets a tooltip for the busy periods canvas. If the mouse pointer * hovers over a busy period for which extended free/busy (XFB) data * could be extracted from the vfreebusy calendar object, the tooltip * will be shown (currently displays the summary and the location of * for the busy period, if available). See EMeetingXfbData for a reference. * */ static gboolean e_meeting_time_selector_on_canvas_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data) { EMeetingTimeSelector *mts = NULL; EMeetingAttendee *attendee = NULL; EMeetingFreeBusyPeriod *period = NULL; EMeetingXfbData *xfb = NULL; GtkScrollable *scrollable = NULL; GtkAdjustment *adjustment = NULL; const GArray *periods = NULL; gint scroll_x = 0; gint scroll_y = 0; gint mouse_x = 0; gint row = 0; gint first_idx = 0; gint ii = 0; gchar *tt_text = NULL; g_return_val_if_fail (GNOME_IS_CANVAS (widget), FALSE); g_return_val_if_fail (GTK_IS_TOOLTIP (tooltip), FALSE); g_return_val_if_fail (E_IS_MEETING_TIME_SELECTOR (user_data), FALSE); mts = E_MEETING_TIME_SELECTOR (user_data); scrollable = GTK_SCROLLABLE (widget); adjustment = gtk_scrollable_get_hadjustment (scrollable); scroll_x = (gint) gtk_adjustment_get_value (adjustment); adjustment = gtk_scrollable_get_vadjustment (scrollable); scroll_y = (gint) gtk_adjustment_get_value (adjustment); /* calculate the attendee index (row) we're at */ row = (scroll_y + y) / mts->row_height; /* no tooltip if we have no attendee in the row */ if (row > e_meeting_store_count_actual_attendees (mts->model) - 1) return FALSE; /* no tooltip if attendee has no calendar info */ attendee = e_meeting_store_find_attendee_at_row (mts->model, row); g_return_val_if_fail (E_IS_MEETING_ATTENDEE (attendee), FALSE); if (!e_meeting_attendee_get_has_calendar_info (attendee)) return FALSE; /* get the attendee's busy times array */ periods = e_meeting_attendee_get_busy_periods (attendee); g_return_val_if_fail (periods != NULL, FALSE); g_return_val_if_fail (periods->len > 0, FALSE); /* no tooltip if no busy period reaches into the current canvas area */ first_idx = e_meeting_attendee_find_first_busy_period ( attendee, &(mts->first_date_shown)); if (first_idx < 0) return FALSE; /* calculate the mouse tip x position in the canvas area */ mouse_x = x + scroll_x; /* find out whether mouse_x lies inside a busy * period (we start with the index of the first * one reaching into the current canvas area) */ for (ii = first_idx; ii < periods->len; ii++) { EMeetingFreeBusyPeriod *p = NULL; gint sx = 0; gint ex = 0; p = &(g_array_index ( periods, EMeetingFreeBusyPeriod, ii)); /* meeting start time x position */ sx = e_meeting_time_selector_calculate_time_position ( mts, &(p->start)); /* meeting end time x position */ ex = e_meeting_time_selector_calculate_time_position ( mts, &(p->end)); if ((mouse_x >= sx) && (mouse_x <= ex)) { /* found busy period the mouse tip is over */ period = p; break; } } /* no tooltip if we did not find a busy period under * the mouse pointer */ if (period == NULL) return FALSE; /* get the extended free/busy data * (no tooltip if none available) */ xfb = &(period->xfb); if ((xfb->summary == NULL) && (xfb->location == NULL)) return FALSE; /* Create the tooltip text. The data sent by the server will * have been validated for UTF-8 conformance (and possibly * forced into) as well as length-limited by a call to the * e_meeting_xfb_utf8_string_new_from_ical() function in * process_free_busy_comp_get_xfb() (e-meeting-store.c) */ if (xfb->summary && xfb->location) tt_text = g_strdup_printf (_("Summary: %s\nLocation: %s"), xfb->summary, xfb->location); else if (xfb->summary) tt_text = g_strdup_printf (_("Summary: %s"), xfb->summary); else if (xfb->location) tt_text = g_strdup_printf (_("Location: %s"), xfb->location); else g_return_val_if_reached (FALSE); /* set XFB information as tooltip text */ gtk_tooltip_set_text (tooltip, tt_text); g_free (tt_text); return TRUE; } /* 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) { GtkAllocation allocation; gint height; gtk_widget_get_allocation (mts->display_main, &allocation); height = mts->row_height * (e_meeting_store_count_actual_attendees (mts->model) + 2); height = MAX (height, allocation.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) { EMeetingTime first_time, last_time, drag_time, *time_to_set; gint scroll_x, scroll_y, canvas_width; gboolean set_both_times = FALSE; GtkAllocation allocation; /* Get the x coords of visible part of the canvas. */ gnome_canvas_get_scroll_offsets ( GNOME_CANVAS (mts->display_main), &scroll_x, &scroll_y); gtk_widget_get_allocation (mts->display_main, &allocation); canvas_width = 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->all_day) { 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; } } else { if (first_time.hour > 0 || first_time.minute > 0) g_date_add_days (&first_time.date, 1); first_time.hour = 0; first_time.minute = 0; last_time.hour = 0; last_time.minute = 0; } 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->all_day) { 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; } } else { if (drag_time.hour > 12) g_date_add_days (&drag_time.date, 1); drag_time.hour = 0; drag_time.minute = 0; } e_meeting_time_selector_fix_time_overflows (&drag_time); /* Now make sure we are between first_time & last_time. */ if (e_meeting_time_compare_times (&drag_time, &first_time) < 0) drag_time = first_time; if (e_meeting_time_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_compare_times (time_to_set, &drag_time) == 0) return; /* Don't let an empty occur for all day events */ if (mts->all_day && mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START && e_meeting_time_compare_times (&mts->meeting_end_time, &drag_time) == 0) return; else if (mts->all_day && mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_END && e_meeting_time_compare_times (&mts->meeting_start_time, &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_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); if (set_both_times || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_END || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) g_signal_emit (mts, signals[CHANGED], 0); } /* 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; EMeetingTime 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; GtkAllocation allocation; mts = E_MEETING_TIME_SELECTOR (data); /* Return if we don't need to scroll yet. */ if (mts->scroll_count-- > 0) 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); gtk_widget_get_allocation (mts->display_main, &allocation); canvas_width = 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->all_day) { if (mts->zoomed_out) { drag_time.minute = 0; } else { drag_time.minute -= drag_time.minute % 30; } } else { drag_time.hour = 0; drag_time.minute = 0; } } else { e_meeting_time_selector_calculate_time ( mts, scroll_x, &drag_time); if (!mts->all_day) { 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; } } else { if (drag_time.hour > 0 || drag_time.minute > 0) g_date_add_days (&drag_time.date, 1); drag_time.hour = 0; drag_time.minute = 0; } } 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_compare_times (time_to_set, &drag_time) == 0) goto scroll; /* Don't let an empty occur for all day events */ if (mts->all_day && mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START && e_meeting_time_compare_times (&mts->meeting_end_time, &drag_time) == 0) goto scroll; else if (mts->all_day && mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_END && e_meeting_time_compare_times (&mts->meeting_start_time, &drag_time) == 0) goto scroll; *time_to_set = drag_time; /* Check if the start time and end time need to be switched. */ if (e_meeting_time_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); if (set_both_times || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_END || mts->dragging_position == E_MEETING_TIME_SELECTOR_POS_START) g_signal_emit (mts, signals[CHANGED], 0); scroll: /* 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. */ 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); 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) { e_date_edit_set_date_and_time_of_day ( E_DATE_EDIT (mts->start_date_edit), g_date_get_year (&mts->meeting_start_time.date), g_date_get_month (&mts->meeting_start_time.date), g_date_get_day (&mts->meeting_start_time.date), mts->meeting_start_time.hour, mts->meeting_start_time.minute); } /* This updates the GnomeDateEdit widget displaying the meeting end time. */ static void e_meeting_time_selector_update_end_date_edit (EMeetingTimeSelector *mts) { GDate date; date = mts->meeting_end_time.date; if (mts->all_day) g_date_subtract_days (&date, 1); e_date_edit_set_date_and_time_of_day ( E_DATE_EDIT (mts->end_date_edit), g_date_get_year (&date), g_date_get_month (&date), g_date_get_day (&date), mts->meeting_end_time.hour, mts->meeting_end_time.minute); } /* 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; gint new_scroll_x; GtkAllocation allocation; EMeetingTime time; /* 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. */ if (e_meeting_time_selector_get_meeting_time_positions (mts, &start_x, &end_x)) { time.date = mts->meeting_start_time.date; time.hour = 0; time.minute = 0; start_x = e_meeting_time_selector_calculate_time_position (mts, &time); } gnome_canvas_get_scroll_offsets ( GNOME_CANVAS (mts->display_main), &scroll_x, &scroll_y); gtk_widget_get_allocation (mts->display_main, &allocation); if (start_x > scroll_x && end_x <= scroll_x + allocation.width) return; 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, E_MEETING_TIME_SELECTOR_DAYS_START_BEFORE); 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 (EMeetingTime *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); } } /* * 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, EMeetingTime *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 EMeetingTime 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, EMeetingTime *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_get_julian (&mtstime->date) - g_date_get_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; } static void row_inserted_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { EMeetingTimeSelector *mts = E_MEETING_TIME_SELECTOR (data); gint row = gtk_tree_path_get_indices (path)[0]; /* Update the scroll region. */ e_meeting_time_selector_update_main_canvas_scroll_region (mts); /* Redraw */ gtk_widget_queue_draw (mts->display_top); gtk_widget_queue_draw (mts->display_main); /* Get the latest free/busy info */ e_meeting_time_selector_refresh_free_busy (mts, row, FALSE); } static void row_changed_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { EMeetingTimeSelector *mts = E_MEETING_TIME_SELECTOR (data); gint row = gtk_tree_path_get_indices (path)[0]; /* Get the latest free/busy info */ e_meeting_time_selector_refresh_free_busy (mts, row, FALSE); } static void row_deleted_cb (GtkTreeModel *model, GtkTreePath *path, gpointer data) { EMeetingTimeSelector *mts = E_MEETING_TIME_SELECTOR (data); /* Update the scroll region. */ e_meeting_time_selector_update_main_canvas_scroll_region (mts); /* Redraw */ gtk_widget_queue_draw (mts->display_top); gtk_widget_queue_draw (mts->display_main); } #define REFRESH_PAUSE 5 static gboolean free_busy_timeout_refresh (gpointer data) { EMeetingTimeSelector *mts = E_MEETING_TIME_SELECTOR (data); /* Update all free/busy info, so we use the new template uri */ e_meeting_time_selector_refresh_free_busy (mts, 0, TRUE); mts->fb_refresh_not = 0; return FALSE; } static void free_busy_template_changed_cb (EMeetingTimeSelector *mts) { /* Wait REFRESH_PAUSE before refreshing, using the latest uri value */ if (mts->fb_refresh_not != 0) g_source_remove (mts->fb_refresh_not); mts->fb_refresh_not = g_timeout_add_seconds ( REFRESH_PAUSE, free_busy_timeout_refresh, mts); }