diff options
-rw-r--r-- | plugins/itip-formatter/ChangeLog | 28 | ||||
-rw-r--r-- | plugins/itip-formatter/itip-formatter.c | 548 | ||||
-rw-r--r-- | plugins/itip-formatter/itip-view.c | 378 | ||||
-rw-r--r-- | plugins/itip-formatter/itip-view.h | 33 | ||||
-rw-r--r-- | plugins/itip-formatter/org-gnome-itip-formatter.eplug.in | 6 |
5 files changed, 953 insertions, 40 deletions
diff --git a/plugins/itip-formatter/ChangeLog b/plugins/itip-formatter/ChangeLog index c2fbae2bad..82154dfc23 100644 --- a/plugins/itip-formatter/ChangeLog +++ b/plugins/itip-formatter/ChangeLog @@ -1,3 +1,31 @@ +2004-12-29 JP Rosevear <jpr@novell.com> + + * itip-view.h: new protos + + * itip-view.c (format_date_and_time_x): don't draw the leading + zero in 12hr clock mode for the hour + (set_sender_text): make intro statements closer to the UI design + (set_description_text): display description + (set_info_items): show info items, messages with icons + (set_progress_text): show progress text item (for + loading/searching calendars) + (set_one_button): add a response button + (set_buttons): set response buttons based on mode + (itip_view_destroy): clear info items + (itip_view_class_init): add response signal + (itip_view_init): new areas for description, info items, buttons + (itip_view_set_description): accessor + (itip_view_get_description): ditto + (itip_view_add_info_item): add an info item to the display + (itip_view_clear_info_items): clear all items + (itip_view_set_progress): set the progress message + + * itip-formatter.c: move over calendar loading, searching code, + set more itip view properties + + * org-gnome-itip-formatter.eplug.in: add a config page item, + doesn't do much right now + 2004-12-22 JP Rosevear <jpr@novell.com> * Initial checkin of new itip formatter diff --git a/plugins/itip-formatter/itip-formatter.c b/plugins/itip-formatter/itip-formatter.c index c16cc24826..7c8a7ed9ab 100644 --- a/plugins/itip-formatter/itip-formatter.c +++ b/plugins/itip-formatter/itip-formatter.c @@ -35,17 +35,28 @@ #include <camel/camel-mime-message.h> #include <libecal/e-cal.h> #include <libecal/e-cal-time-util.h> +#include <libedataserverui/e-source-option-menu.h> +#include <libedataserverui/e-source-selector.h> #include <gtkhtml/gtkhtml-embedded.h> #include <mail/em-format-hook.h> +#include <mail/em-config.h> #include <mail/em-format-html.h> #include <e-util/e-account-list.h> #include <e-util/e-icon-factory.h> #include <calendar/gui/itip-utils.h> +#include <calendar/common/authentication.h> #include "itip-view.h" #define CLASSID "itip://" void format_itip (EPlugin *ep, EMFormatHookTarget *target); +GtkWidget *itip_formatter_page_factory (EPlugin *ep, EConfigHookItemFactoryData *hook_data); + +/* FIXME We should include these properly */ +icaltimezone *calendar_config_get_icaltimezone (void); +void calendar_config_init (void); +char *calendar_config_get_primary_calendar (void); +char *calendar_config_get_primary_tasks (void); typedef struct { EMFormatHTMLPObject pobject; @@ -58,12 +69,7 @@ typedef struct { ECal *current_ecal; ECalSourceType type; - char action; gboolean rsvp; - - GtkWidget *ok; - GtkWidget *hbox; - GtkWidget *vbox; char *vcalendar; ECalComponent *comp; @@ -87,6 +93,350 @@ typedef struct { gint view_only; } FormatItipPObject; +typedef struct { + FormatItipPObject *pitip; + char *uid; + int count; + gboolean show_selector; +} EItipControlFindData; + + +typedef void (* FormatItipOpenFunc) (ECal *ecal, ECalendarStatus status, gpointer data); + +static void +cal_opened_cb (ECal *ecal, ECalendarStatus status, gpointer data) +{ + FormatItipPObject *pitip = data; + ESource *source; + ECalSourceType source_type; + icaltimezone *zone; + + source_type = e_cal_get_source_type (ecal); + source = e_cal_get_source (ecal); + + g_signal_handlers_disconnect_matched (ecal, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, cal_opened_cb, NULL); + + if (status != E_CALENDAR_STATUS_OK) { + itip_view_set_progress (ITIP_VIEW (pitip->view), "Failed to load at least one calendar"); + + g_hash_table_remove (pitip->ecals[source_type], e_source_peek_uid (source)); + + return; + } + + zone = calendar_config_get_icaltimezone (); + e_cal_set_default_timezone (ecal, zone, NULL); + + pitip->current_ecal = ecal; + +// set_ok_sens (itip); +} + +static ECal * +start_calendar_server (FormatItipPObject *pitip, ESource *source, ECalSourceType type, FormatItipOpenFunc func, gpointer data) +{ + ECal *ecal; + + ecal = g_hash_table_lookup (pitip->ecals[type], e_source_peek_uid (source)); + if (ecal) { + pitip->current_ecal = ecal; + + itip_view_set_progress (ITIP_VIEW (pitip->view), NULL); +// set_ok_sens (itip); + return ecal; + } + + ecal = auth_new_cal_from_source (source, type); + g_signal_connect (G_OBJECT (ecal), "cal_opened", G_CALLBACK (func), data); + + g_hash_table_insert (pitip->ecals[type], g_strdup (e_source_peek_uid (source)), ecal); + + e_cal_open_async (ecal, TRUE); + + return ecal; +} + +static ECal * +start_calendar_server_by_uid (FormatItipPObject *pitip, const char *uid, ECalSourceType type) +{ + int i; + + for (i = 0; i < E_CAL_SOURCE_TYPE_LAST; i++) { + ESource *source; + + source = e_source_list_peek_source_by_uid (pitip->source_lists[i], uid); + if (source) + return start_calendar_server (pitip, source, type, cal_opened_cb, pitip); + } + + return NULL; +} + +static void +source_selected_cb (ESourceOptionMenu *esom, ESource *source, gpointer data) +{ + FormatItipPObject *pitip = data; + + g_message ("Source selected"); + + /* FIXME turn off buttons while we check the calendar for being open? */ + + start_calendar_server (pitip, source, pitip->type, cal_opened_cb, pitip); +} + +static void +find_cal_opened_cb (ECal *ecal, ECalendarStatus status, gpointer data) +{ + EItipControlFindData *fd = data; + FormatItipPObject *pitip = fd->pitip; + ESource *source; + ECalSourceType source_type; + icalcomponent *icalcomp; + icaltimezone *zone; + + source_type = e_cal_get_source_type (ecal); + source = e_cal_get_source (ecal); + + fd->count--; + + g_signal_handlers_disconnect_matched (ecal, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, find_cal_opened_cb, NULL); + + if (status != E_CALENDAR_STATUS_OK) { + g_hash_table_remove (pitip->ecals[source_type], e_source_peek_uid (source)); + + goto cleanup; + } + + if (e_cal_get_object (ecal, fd->uid, NULL, &icalcomp, NULL)) { + icalcomponent_free (icalcomp); + + pitip->current_ecal = ecal; + + itip_view_set_progress (ITIP_VIEW (pitip->view), NULL); +// set_ok_sens (fd->itip); + } + + zone = calendar_config_get_icaltimezone (); + e_cal_set_default_timezone (ecal, zone, NULL); + + cleanup: + if (fd->count == 0) { + /* FIXME the box check is to see if the buttons are displayed i think */ + if (fd->show_selector && !pitip->current_ecal /*&& pitip->vbox*/) { + GtkWidget *esom; + ESource *source = NULL; + char *uid; + + switch (pitip->type) { + case E_CAL_SOURCE_TYPE_EVENT: + uid = calendar_config_get_primary_calendar (); + break; + case E_CAL_SOURCE_TYPE_TODO: + uid = calendar_config_get_primary_tasks (); + break; + default: + uid = NULL; + g_assert_not_reached (); + } + + if (uid) { + source = e_source_list_peek_source_by_uid (pitip->source_lists[pitip->type], uid); + g_free (uid); + } + + /* Try to create a default if there isn't one */ + if (!source) + source = e_source_list_peek_source_any (pitip->source_lists[pitip->type]); + + g_message ("Picking any source"); + esom = e_source_option_menu_new (pitip->source_lists[pitip->type]); + /* FIXME used to force the data to be kept alive, still do this? */ + g_signal_connect (esom, "source_selected", G_CALLBACK (source_selected_cb), fd->pitip); + + //gtk_box_pack_start (GTK_BOX (pitip->vbox), esom, FALSE, TRUE, 0); + gtk_widget_show (esom); + + /* FIXME What if there is no source? */ + if (source) { + e_source_option_menu_select (E_SOURCE_OPTION_MENU (esom), source); + itip_view_set_progress (ITIP_VIEW (pitip->view), NULL); + } + } else { + /* FIXME Display error message to user */ + } + + g_free (fd->uid); + g_free (fd); + } +} + +static void +find_server (FormatItipPObject *pitip, ECalComponent *comp, gboolean show_selector) +{ + EItipControlFindData *fd = NULL; + GSList *groups, *l; + const char *uid; + + e_cal_component_get_uid (comp, &uid); + + itip_view_set_progress (ITIP_VIEW (pitip->view), "Searching for an existing version of this appointment"); + + groups = e_source_list_peek_groups (pitip->source_lists[pitip->type]); + for (l = groups; l; l = l->next) { + ESourceGroup *group; + GSList *sources, *m; + + group = l->data; + + sources = e_source_group_peek_sources (group); + for (m = sources; m; m = m->next) { + ESource *source; + ECal *ecal; + + source = m->data; + + if (!fd) { + fd = g_new0 (EItipControlFindData, 1); + fd->pitip = pitip; + fd->uid = g_strdup (uid); + fd->show_selector = show_selector; + } + fd->count++; + + ecal = start_calendar_server (pitip, source, pitip->type, find_cal_opened_cb, fd); + } + } +} + +static void +cleanup_ecal (ECal *ecal) +{ + /* Clean up any signals */ + g_signal_handlers_disconnect_matched (ecal, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, cal_opened_cb, NULL); + g_signal_handlers_disconnect_matched (ecal, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, find_cal_opened_cb, NULL); + + g_object_unref (ecal); +} + +static icalproperty * +find_attendee (icalcomponent *ical_comp, const char *address) +{ + icalproperty *prop; + + if (address == NULL) + return NULL; + + for (prop = icalcomponent_get_first_property (ical_comp, ICAL_ATTENDEE_PROPERTY); + prop != NULL; + prop = icalcomponent_get_next_property (ical_comp, ICAL_ATTENDEE_PROPERTY)) { + icalvalue *value; + const char *attendee; + char *text; + + value = icalproperty_get_value (prop); + if (!value) + continue; + + attendee = icalvalue_get_string (value); + + text = g_strdup (itip_strip_mailto (attendee)); + text = g_strstrip (text); + if (!g_strcasecmp (address, text)) { + g_free (text); + break; + } + g_free (text); + } + + return prop; +} + +static gboolean +change_status (icalcomponent *ical_comp, const char *address, icalparameter_partstat status) +{ + icalproperty *prop; + + prop = find_attendee (ical_comp, address); + if (prop) { + icalparameter *param; + + icalproperty_remove_parameter (prop, ICAL_PARTSTAT_PARAMETER); + param = icalparameter_new_partstat (status); + icalproperty_add_parameter (prop, param); + } else { + icalparameter *param; + + if (address != NULL) { + prop = icalproperty_new_attendee (address); + icalcomponent_add_property (ical_comp, prop); + + param = icalparameter_new_role (ICAL_ROLE_OPTPARTICIPANT); + icalproperty_add_parameter (prop, param); + + param = icalparameter_new_partstat (status); + icalproperty_add_parameter (prop, param); + } else { + EAccount *a; + + a = itip_addresses_get_default (); + + prop = icalproperty_new_attendee (a->id->address); + icalcomponent_add_property (ical_comp, prop); + + param = icalparameter_new_cn (a->id->name); + icalproperty_add_parameter (prop, param); + + param = icalparameter_new_role (ICAL_ROLE_REQPARTICIPANT); + icalproperty_add_parameter (prop, param); + + param = icalparameter_new_partstat (status); + icalproperty_add_parameter (prop, param); + } + } + + return TRUE; +} + +static void +update_item (FormatItipPObject *pitip) +{ + struct icaltimetype stamp; + icalproperty *prop; + icalcomponent *clone; +// GtkWidget *dialog; + GError *error = NULL; + + /* Set X-MICROSOFT-CDO-REPLYTIME to record the time at which + * the user accepted/declined the request. (Outlook ignores + * SEQUENCE in REPLY reponses and instead requires that each + * updated response have a later REPLYTIME than the previous + * one.) This also ends up getting saved in our own copy of + * the meeting, though there's currently no way to see that + * information (unless it's being saved to an Exchange folder + * and you then look at it in Outlook). + */ + stamp = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ()); + prop = icalproperty_new_x (icaltime_as_ical_string (stamp)); + icalproperty_set_x_name (prop, "X-MICROSOFT-CDO-REPLYTIME"); + icalcomponent_add_property (pitip->ical_comp, prop); + + clone = icalcomponent_new_clone (pitip->ical_comp); + icalcomponent_add_component (pitip->top_level, clone); + icalcomponent_set_method (pitip->top_level, pitip->method); + + if (!e_cal_receive_objects (pitip->current_ecal, pitip->top_level, &error)) { + /* FIXME e-error */ +// dialog = gnome_warning_dialog (error->message); + g_error_free (error); + } else { + /* FIXME I think we should do nothing */ +// dialog = gnome_ok_dialog (_("Update complete\n")); + } +// gnome_dialog_run_and_close (GNOME_DIALOG (dialog)); + + icalcomponent_remove_component (pitip->top_level, clone); +} + static icalcomponent * get_next (icalcompiter *iter) { @@ -191,6 +541,42 @@ extract_itip_data (FormatItipPObject *pitip) // show_current (itip); } +static void +view_response_cb (GtkWidget *widget, ItipViewResponse response, gpointer data) +{ + FormatItipPObject *pitip = data; + gboolean status = FALSE; + + switch (response) { + case ITIP_VIEW_RESPONSE_ACCEPT: + status = change_status (pitip->ical_comp, pitip->my_address, + ICAL_PARTSTAT_ACCEPTED); + if (status) { + e_cal_component_rescan (pitip->comp); + update_item (pitip); + } + break; + case ITIP_VIEW_RESPONSE_TENTATIVE: + status = change_status (pitip->ical_comp, pitip->my_address, + ICAL_PARTSTAT_TENTATIVE); + if (status) { + e_cal_component_rescan (pitip->comp); + update_item (pitip); + } + break; + case ITIP_VIEW_RESPONSE_DECLINE: + status = change_status (pitip->ical_comp, pitip->my_address, + ICAL_PARTSTAT_DECLINED); + if (status) { + e_cal_component_rescan (pitip->comp); + update_item (pitip); + } + break; + default: + break; + } +} + static gboolean format_itip_object (EMFormatHTML *efh, GtkHTMLEmbedded *eb, EMFormatHTMLPObject *pobject) { @@ -199,16 +585,58 @@ format_itip_object (EMFormatHTML *efh, GtkHTMLEmbedded *eb, EMFormatHTMLPObject ECalComponentOrganizer organizer; ECalComponentDateTime datetime; icaltimezone *from_zone, *to_zone; + GString *gstring = NULL; + GSList *list, *l; const char *string; - + int i; + + /* Source Lists and open ecal clients */ + for (i = 0; i < E_CAL_SOURCE_TYPE_LAST; i++) { + if (!e_cal_get_sources (&pitip->source_lists[E_CAL_SOURCE_TYPE_EVENT], E_CAL_SOURCE_TYPE_EVENT, NULL)) + /* FIXME More error handling? */ + pitip->source_lists[i] = NULL; + + /* Initialize the ecal hashes */ + pitip->ecals[i] = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, cleanup_ecal); + } + /* FIXME Error handling? */ + /* FIXME Handle multiple VEVENTS with the same UID, ie detached instances */ extract_itip_data (pitip); pitip->view = itip_view_new (); gtk_widget_show (pitip->view); - itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_REQUEST); - + switch (pitip->method) { + case ICAL_METHOD_PUBLISH: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_PUBLISH); + break; + case ICAL_METHOD_REQUEST: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_REQUEST); + break; + case ICAL_METHOD_REPLY: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_REPLY); + break; + case ICAL_METHOD_ADD: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_ADD); + break; + case ICAL_METHOD_CANCEL: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_CANCEL); + break; + case ICAL_METHOD_REFRESH: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_REFRESH); + break; + case ICAL_METHOD_COUNTER: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_COUNTER); + break; + case ICAL_METHOD_DECLINECOUNTER: + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_DECLINECOUNTER); + break; + default: + /* FIXME What to do here? */ + itip_view_set_mode (ITIP_VIEW (pitip->view), ITIP_VIEW_MODE_ERROR); + } + e_cal_component_get_organizer (pitip->comp, &organizer); itip_view_set_organizer (ITIP_VIEW (pitip->view), organizer.cn ? organizer.cn : itip_strip_mailto (organizer.value)); /* FIXME, do i need to strip the sentby somehow? Maybe with camel? */ @@ -220,6 +648,23 @@ format_itip_object (EMFormatHTML *efh, GtkHTMLEmbedded *eb, EMFormatHTMLPObject e_cal_component_get_location (pitip->comp, &string); itip_view_set_location (ITIP_VIEW (pitip->view), string); + e_cal_component_get_location (pitip->comp, &string); + itip_view_set_location (ITIP_VIEW (pitip->view), string); + + e_cal_component_get_description_list (pitip->comp, &list); + for (l = list; l; l = l->next) { + ECalComponentText *text = l->data; + + if (!gstring) + gstring = g_string_new (text->value); + else + g_string_append_printf (gstring, "\n\n%s", text->value); + } + e_cal_component_free_text_list (list); + + itip_view_set_description (ITIP_VIEW (pitip->view), gstring->str); + g_string_free (gstring, TRUE); + to_zone = calendar_config_get_icaltimezone (); e_cal_component_get_dtstart (pitip->comp, &datetime); @@ -254,8 +699,18 @@ format_itip_object (EMFormatHTML *efh, GtkHTMLEmbedded *eb, EMFormatHTMLPObject } e_cal_component_free_datetime (&datetime); + /* Info area items */ + itip_view_add_info_item (ITIP_VIEW (pitip->view), ITIP_VIEW_INFO_ITEM_TYPE_INFO, "This meeting occurs weekly indefinitely"); + itip_view_add_info_item (ITIP_VIEW (pitip->view), ITIP_VIEW_INFO_ITEM_TYPE_WARNING, "An appointment in the calendar conflicts with this meeting"); + gtk_container_add (GTK_CONTAINER (eb), pitip->view); + gtk_widget_set_usize (pitip->view, 640, -1); + + g_signal_connect (pitip->view, "response", G_CALLBACK (view_response_cb), pitip); + /* FIXME Show selector should be handled in the itip view */ + find_server (pitip, pitip->comp, TRUE); + return TRUE; } @@ -273,3 +728,80 @@ format_itip (EPlugin *ep, EMFormatHookTarget *target) camel_stream_printf (target->stream, "<td valign=top><object classid=\"%s\"></object></td><td width=100%% valign=top>", CLASSID); camel_stream_printf (target->stream, "</td></tr></table>"); } + +GtkWidget * +itip_formatter_page_factory (EPlugin *ep, EConfigHookItemFactoryData *hook_data) +{ +// EMConfigTargetPrefs *target = (EMConfigTargetPrefs *) hook_data->config->target; + GtkWidget *page; + GtkWidget *tab_label; + GtkWidget *frame; + GtkWidget *frame_label; + GtkWidget *padding_label; + GtkWidget *hbox; + GtkWidget *inner_vbox; + GtkWidget *check; + GtkWidget *check_gaim; + + /* A structure to pass some stuff around */ +// stuff = g_new0 (struct bbdb_stuff, 1); +// stuff->target = target; + + /* Create a new notebook page */ + page = gtk_vbox_new (FALSE, 0); + GTK_CONTAINER (page)->border_width = 12; + tab_label = gtk_label_new (_("Meetings")); + gtk_notebook_append_page (GTK_NOTEBOOK (hook_data->parent), page, tab_label); + + /* Frame */ + frame = gtk_vbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (page), frame, FALSE, FALSE, 0); + + /* "Automatic Contacts" */ + frame_label = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (frame_label), _("<span weight=\"bold\">General</span>")); + GTK_MISC (frame_label)->xalign = 0.0; + gtk_box_pack_start (GTK_BOX (frame), frame_label, FALSE, FALSE, 0); + + /* Indent/padding */ + hbox = gtk_hbox_new (FALSE, 12); + gtk_box_pack_start (GTK_BOX (frame), hbox, FALSE, TRUE, 0); + padding_label = gtk_label_new (""); + gtk_box_pack_start (GTK_BOX (hbox), padding_label, FALSE, FALSE, 0); + inner_vbox = gtk_vbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (hbox), inner_vbox, FALSE, FALSE, 0); + + /* Enable BBDB checkbox */ + check = gtk_check_button_new_with_mnemonic (_("_Delete message after acting")); +// gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check), gconf_client_get_bool (target->gconf, GCONF_KEY_ENABLE, NULL)); +// g_signal_connect (GTK_TOGGLE_BUTTON (check), "toggled", G_CALLBACK (enable_toggled_cb), stuff); + gtk_box_pack_start (GTK_BOX (inner_vbox), check, FALSE, FALSE, 0); +// stuff->check = check; + + /* "Instant Messaging Contacts" */ + frame = gtk_vbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (page), frame, TRUE, TRUE, 24); + + frame_label = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL (frame_label), _("<span weight=\"bold\">Conflict Search</span>")); + GTK_MISC (frame_label)->xalign = 0.0; + gtk_box_pack_start (GTK_BOX (frame), frame_label, FALSE, FALSE, 0); + + /* Indent/padding */ + hbox = gtk_hbox_new (FALSE, 12); + gtk_box_pack_start (GTK_BOX (frame), hbox, FALSE, TRUE, 0); + padding_label = gtk_label_new (""); + gtk_box_pack_start (GTK_BOX (hbox), padding_label, FALSE, FALSE, 0); + inner_vbox = gtk_vbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (hbox), inner_vbox, FALSE, FALSE, 0); + + /* Enable Gaim Checkbox */ + check_gaim = gtk_label_new (_("Select the calendars to search for meeting conflicts")); + gtk_box_pack_start (GTK_BOX (inner_vbox), check_gaim, FALSE, FALSE, 0); +// stuff->check_gaim = check_gaim; + + gtk_widget_show_all (page); + + return page; +} + diff --git a/plugins/itip-formatter/itip-view.c b/plugins/itip-formatter/itip-view.c index e58506605b..a1a1e8c12a 100644 --- a/plugins/itip-formatter/itip-view.c +++ b/plugins/itip-formatter/itip-view.c @@ -20,6 +20,10 @@ * */ +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + #include <string.h> #include <glib.h> #include <gtk/gtk.h> @@ -36,6 +40,7 @@ #include <e-util/e-account-list.h> #include <e-util/e-icon-factory.h> #include <e-util/e-time-utils.h> +#include <e-util/e-gtk-utils.h> #include <calendar/gui/itip-utils.h> #include "itip-view.h" @@ -43,6 +48,12 @@ G_DEFINE_TYPE (ItipView, itip_view, GTK_TYPE_HBOX); +typedef struct { + ItipViewInfoItemType type; + + char *message; +} ItipViewInfoItem; + struct _ItipViewPrivate { ItipViewMode mode; @@ -66,8 +77,28 @@ struct _ItipViewPrivate { GtkWidget *end_header; GtkWidget *end_label; struct tm *end_tm; + + GtkWidget *info_box; + GSList *info_items; + + GtkWidget *description_label; + char *description; + + GtkWidget *progress_box; + GtkWidget *progress_label; + char *progress; + + GtkWidget *button_box; +}; + +/* Signal IDs */ +enum { + RESPONSE, + LAST_SIGNAL }; +static guint signals[LAST_SIGNAL] = { 0 }; + static void format_date_and_time_x (struct tm *date_tm, struct tm *current_tm, @@ -100,11 +131,11 @@ format_date_and_time_x (struct tm *date_tm, if (!show_zero_seconds && date_tm->tm_sec == 0) /* strftime format of a weekday, a date and a time, in 12-hour format, without seconds. */ - format = _("Today %I:%M %p"); + format = _("Today %l:%M %p"); else /* strftime format of a weekday, a date and a time, in 12-hour format. */ - format = _("Today %I:%M:%S %p"); + format = _("Today %l:%M:%S %p"); } /* Tomorrow */ @@ -126,11 +157,11 @@ format_date_and_time_x (struct tm *date_tm, if (!show_zero_seconds && date_tm->tm_sec == 0) /* strftime format of a weekday, a date and a time, in 12-hour format, without seconds. */ - format = _("%A, %B %e %I:%M %p"); + format = _("%A, %B %e %l:%M %p"); else /* strftime format of a weekday, a date and a time, in 12-hour format. */ - format = _("%A, %B %e %I:%M:%S %p"); + format = _("%A, %B %e %l:%M:%S %p"); } /* Within 7 days */ @@ -152,11 +183,11 @@ format_date_and_time_x (struct tm *date_tm, if (!show_zero_seconds && date_tm->tm_sec == 0) /* strftime format of a weekday, a date and a time, in 12-hour format, without seconds. */ - format = _("%A %I:%M %p"); + format = _("%A %l:%M %p"); else /* strftime format of a weekday, a date and a time, in 12-hour format. */ - format = _("%A %I:%M:%S %p"); + format = _("%A %l:%M:%S %p"); } /* This Year */ @@ -178,11 +209,11 @@ format_date_and_time_x (struct tm *date_tm, if (!show_zero_seconds && date_tm->tm_sec == 0) /* strftime format of a weekday, a date and a time, in 12-hour format, without seconds. */ - format = _("%A, %B %e %I:%M %p"); + format = _("%A, %B %e %l:%M %p"); else /* strftime format of a weekday, a date and a time, in 12-hour format. */ - format = _("%A, %B %e %I:%M:%S %p"); + format = _("%A, %B %e %l:%M:%S %p"); } } else { if (!show_midnight && date_tm->tm_hour == 0 @@ -202,11 +233,11 @@ format_date_and_time_x (struct tm *date_tm, if (!show_zero_seconds && date_tm->tm_sec == 0) /* strftime format of a weekday, a date and a time, in 12-hour format, without seconds. */ - format = _("%A, %B %e, %Y %I:%M %p"); + format = _("%A, %B %e, %Y %l:%M %p"); else /* strftime format of a weekday, a date and a time, in 12-hour format. */ - format = _("%A, %B %e, %Y %I:%M:%S %p"); + format = _("%A, %B %e, %Y %l:%M:%S %p"); } } @@ -231,38 +262,45 @@ set_sender_text (ItipView *view) switch (priv->mode) { case ITIP_VIEW_MODE_PUBLISH: if (priv->sentby) - sender = g_strdup_printf (_("<b>%s</b> through %s has published meeting information."), organizer, priv->sentby); + sender = g_strdup_printf (_("<b>%s</b> through %s has published the following meeting information:"), organizer, priv->sentby); else - sender = g_strdup_printf (_("<b>%s</b> has published meeting information."), organizer); + sender = g_strdup_printf (_("<b>%s</b> has published the following meeting information:"), organizer); break; case ITIP_VIEW_MODE_REQUEST: /* FIXME is the delegator stuff handled correctly here? */ if (priv->delegator) { - sender = g_strdup_printf (_("<b>%s</b> requests the presence of %s at a meeting."), organizer, priv->delegator); + sender = g_strdup_printf (_("<b>%s</b> requests the presence of %s at the following meeting:"), organizer, priv->delegator); } else { if (priv->sentby) - sender = g_strdup_printf (_("<b>%s</b> through %s requests your presence at a meeting."), organizer, priv->sentby); + sender = g_strdup_printf (_("<b>%s</b> through %s requests your presence at the following meeting:"), organizer, priv->sentby); else - sender = g_strdup_printf (_("<b>%s</b> requests your presence at a meeting."), organizer); + sender = g_strdup_printf (_("<b>%s</b> requests your presence at the following meeting:"), organizer); } break; case ITIP_VIEW_MODE_ADD: + /* FIXME What text for this? */ if (priv->sentby) - sender = g_strdup_printf (_("<b>%s</b> through %s wishes to add to an existing meeting."), organizer, priv->sentby); + sender = g_strdup_printf (_("<b>%s</b> through %s wishes to add to an existing meeting:"), organizer, priv->sentby); else - sender = g_strdup_printf (_("<b>%s</b> wishes to add to an existing meeting."), organizer); + sender = g_strdup_printf (_("<b>%s</b> wishes to add to an existing meeting:"), organizer); break; case ITIP_VIEW_MODE_REFRESH: - sender = g_strdup_printf (_("<b>%s</b> wishes to receive the latest meeting information."), attendee); + sender = g_strdup_printf (_("<b>%s</b> wishes to receive the latest information for the following meeting:"), attendee); break; case ITIP_VIEW_MODE_REPLY: - sender = g_strdup_printf (_("<b>%s</b> has replied to a meeting invitation."), attendee); + sender = g_strdup_printf (_("<b>%s</b> has accepted the following meeting:"), attendee); break; case ITIP_VIEW_MODE_CANCEL: if (priv->sentby) - sender = g_strdup_printf (_("<b>%s</b> through %s has cancelled a meeting."), organizer, priv->sentby); + sender = g_strdup_printf (_("<b>%s</b> through %s has cancelled the follow meeting:"), organizer, priv->sentby); else - sender = g_strdup_printf (_("<b>%s</b> has cancelled a meeting."), organizer); + sender = g_strdup_printf (_("<b>%s</b> has cancelled the following meeting."), organizer); + break; + case ITIP_VIEW_MODE_COUNTER: + if (priv->sentby) + sender = g_strdup_printf (_("<b>%s</b> through %s has cancelled the follow meeting:"), organizer, priv->sentby); + else + sender = g_strdup_printf (_("<b>%s</b> has cancelled the following meeting."), organizer); break; default: break; @@ -304,6 +342,18 @@ set_location_text (ItipView *view) } static void +set_description_text (ItipView *view) +{ + ItipViewPrivate *priv; + + priv = view->priv; + + gtk_label_set_text (GTK_LABEL (priv->description_label), priv->description); + + priv->description ? gtk_widget_show (priv->description_label) : gtk_widget_hide (priv->description_label); +} + +static void set_start_text (ItipView *view) { ItipViewPrivate *priv; @@ -352,6 +402,139 @@ set_end_text (ItipView *view) } static void +set_info_items (ItipView *view) +{ + ItipViewPrivate *priv; + GSList *l; + + priv = view->priv; + + gtk_container_foreach (GTK_CONTAINER (priv->info_box), (GtkCallback) gtk_widget_destroy, NULL); + + for (l = priv->info_items; l; l = l->next) { + ItipViewInfoItem *item = l->data; + GtkWidget *hbox, *image, *label; + + hbox = gtk_hbox_new (FALSE, 0); + + switch (item->type) { + case ITIP_VIEW_INFO_ITEM_TYPE_INFO: + image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_SMALL_TOOLBAR); + break; + case ITIP_VIEW_INFO_ITEM_TYPE_WARNING: + image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_SMALL_TOOLBAR); + break; + case ITIP_VIEW_INFO_ITEM_TYPE_ERROR: + image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_ERROR, GTK_ICON_SIZE_SMALL_TOOLBAR); + break; + case ITIP_VIEW_INFO_ITEM_TYPE_NONE: + default: + image = NULL; + } + + if (image) { + gtk_widget_show (image); + gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 6); + } + + label = gtk_label_new (item->message); + gtk_widget_show (label); + gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 6); + + gtk_widget_show (hbox); + gtk_box_pack_start (GTK_BOX (priv->info_box), hbox, FALSE, FALSE, 6); + } +} + +static void +set_progress_text (ItipView *view) +{ + ItipViewPrivate *priv; + + priv = view->priv; + + g_message ("Setting progress to: %s", priv->progress); + gtk_label_set_text (GTK_LABEL (priv->progress_label), priv->progress); + + priv->progress ? gtk_widget_show (priv->progress_box) : gtk_widget_hide (priv->progress_box); +} + +#define DATA_RESPONSE_KEY "ItipView::button_response" + +static void +button_clicked_cb (GtkWidget *widget, gpointer data) +{ + ItipViewResponse response; + + response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), DATA_RESPONSE_KEY)); + + g_message ("Response %d", response); + g_signal_emit (G_OBJECT (data), signals[RESPONSE], 0, response); +} + +static void +set_one_button (ItipView *view, char *label, char *stock_id, ItipViewResponse response) +{ + ItipViewPrivate *priv; + GtkWidget *button; + + priv = view->priv; + + button = e_gtk_button_new_with_icon (label, stock_id); + g_object_set_data (G_OBJECT (button), DATA_RESPONSE_KEY, GINT_TO_POINTER (response)); + gtk_widget_show (button); + gtk_container_add (GTK_CONTAINER (priv->button_box), button); + + g_signal_connect (button, "clicked", G_CALLBACK (button_clicked_cb), view); +} + +static void +set_buttons (ItipView *view) +{ + ItipViewPrivate *priv; + + priv = view->priv; + + gtk_container_foreach (GTK_CONTAINER (priv->button_box), (GtkCallback) gtk_widget_destroy, NULL); + + /* Everything gets the open button */ + set_one_button (view, "_Open Calendar", GTK_STOCK_JUMP_TO, ITIP_VIEW_RESPONSE_OPEN); + + switch (priv->mode) { + case ITIP_VIEW_MODE_PUBLISH: + /* FIXME Is this really the right button? */ + set_one_button (view, "_Accept", GTK_STOCK_APPLY, ITIP_VIEW_RESPONSE_ACCEPT); + break; + case ITIP_VIEW_MODE_REQUEST: + set_one_button (view, "_Decline", GTK_STOCK_CANCEL, ITIP_VIEW_RESPONSE_DECLINE); + set_one_button (view, "_Tentative", GTK_STOCK_DIALOG_QUESTION, ITIP_VIEW_RESPONSE_TENTATIVE); + set_one_button (view, "_Accept", GTK_STOCK_APPLY, ITIP_VIEW_RESPONSE_ACCEPT); + break; + case ITIP_VIEW_MODE_ADD: + /* FIXME Right response? */ + set_one_button (view, "_Add", GTK_STOCK_ADD, ITIP_VIEW_RESPONSE_ACCEPT); + break; + case ITIP_VIEW_MODE_REFRESH: + /* FIXME Is this really the right button? */ + set_one_button (view, "_Refresh", GTK_STOCK_REFRESH, ITIP_VIEW_RESPONSE_REFRESH); + break; + case ITIP_VIEW_MODE_REPLY: + /* FIXME Is this really the right button? */ + set_one_button (view, "_Update", GTK_STOCK_REFRESH, ITIP_VIEW_RESPONSE_UPDATE); + break; + case ITIP_VIEW_MODE_CANCEL: + set_one_button (view, "_Update", GTK_STOCK_REFRESH, ITIP_VIEW_RESPONSE_UPDATE); + break; + case ITIP_VIEW_MODE_COUNTER: + break; + case ITIP_VIEW_MODE_DECLINECOUNTER: + break; + default: + break; + } +} + +static void itip_view_destroy (GtkObject *object) { ItipView *view = ITIP_VIEW (object); @@ -365,6 +548,8 @@ itip_view_destroy (GtkObject *object) g_free (priv->location); g_free (priv->start_tm); g_free (priv->end_tm); + + itip_view_clear_info_items (view); g_free (priv); view->priv = NULL; @@ -383,13 +568,22 @@ itip_view_class_init (ItipViewClass *klass) gtkobject_class = GTK_OBJECT_CLASS (klass); gtkobject_class->destroy = itip_view_destroy; + + signals[RESPONSE] = + g_signal_new ("response", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ItipViewClass, response), + NULL, NULL, + gtk_marshal_NONE__INT, + G_TYPE_NONE, 1, G_TYPE_INT); } static void itip_view_init (ItipView *view) { ItipViewPrivate *priv; - GtkWidget *icon, *vbox, *separator, *table; + GtkWidget *icon, *vbox, *separator, *table, *image; priv = g_new0 (ItipViewPrivate, 1); view->priv = priv; @@ -411,6 +605,7 @@ itip_view_init (ItipView *view) /* The first section listing the sender */ /* FIXME What to do if the send and organizer do not match */ priv->sender_label = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (priv->sender_label), 0, 0.5); gtk_widget_show (priv->sender_label); gtk_box_pack_start (GTK_BOX (vbox), priv->sender_label, FALSE, FALSE, 6); @@ -434,11 +629,13 @@ itip_view_init (ItipView *view) /* Location */ priv->location_header = gtk_label_new (_("Location:")); priv->location_label = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (priv->location_header), 0, 0.5); gtk_misc_set_alignment (GTK_MISC (priv->location_label), 0, 0.5); gtk_table_attach (GTK_TABLE (table), priv->location_header, 0, 1, 1, 2, GTK_FILL, 0, 0, 0); gtk_table_attach (GTK_TABLE (table), priv->location_label, 1, 2, 1, 2, GTK_FILL, 0, 0, 0); - priv->start_header = gtk_label_new (_("Starts:")); + /* Start time */ + priv->start_header = gtk_label_new (_("Start time:")); priv->start_label = gtk_label_new (NULL); gtk_misc_set_alignment (GTK_MISC (priv->start_header), 0, 0.5); gtk_misc_set_alignment (GTK_MISC (priv->start_label), 0, 0.5); @@ -446,14 +643,48 @@ itip_view_init (ItipView *view) gtk_table_attach (GTK_TABLE (table), priv->start_header, 0, 1, 2, 3, GTK_FILL, 0, 0, 0); gtk_table_attach (GTK_TABLE (table), priv->start_label, 1, 2, 2, 3, GTK_FILL, 0, 0, 0); - priv->end_header = gtk_label_new (_("Ends:")); + /* End time */ + priv->end_header = gtk_label_new (_("End time:")); priv->end_label = gtk_label_new (NULL); gtk_misc_set_alignment (GTK_MISC (priv->end_header), 0, 0.5); gtk_misc_set_alignment (GTK_MISC (priv->end_label), 0, 0.5); gtk_table_attach (GTK_TABLE (table), priv->end_header, 0, 1, 3, 4, GTK_FILL, 0, 0, 0); gtk_table_attach (GTK_TABLE (table), priv->end_label, 1, 2, 3, 4, GTK_FILL, 0, 0, 0); + + /* Info items */ + priv->info_box = gtk_vbox_new (FALSE, 0); + gtk_widget_show (priv->info_box); + gtk_box_pack_start (GTK_BOX (vbox), priv->info_box, FALSE, FALSE, 6); + + /* Description */ + priv->description_label = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (priv->description_label), TRUE); + gtk_misc_set_alignment (GTK_MISC (priv->description_label), 0, 0.5); +// gtk_box_pack_start (GTK_BOX (vbox), priv->description_label, FALSE, FALSE, 6); + + separator = gtk_hseparator_new (); + gtk_widget_show (separator); + gtk_box_pack_start (GTK_BOX (vbox), separator, FALSE, FALSE, 6); + + /* Progress */ + priv->progress_box = gtk_hbox_new (FALSE, 0); + gtk_box_pack_start (GTK_BOX (vbox), priv->progress_box, FALSE, FALSE, 6); + + image = e_icon_factory_get_image ("stock_animation", E_ICON_SIZE_BUTTON); + gtk_widget_show (image); + gtk_box_pack_start (GTK_BOX (priv->progress_box), image, FALSE, FALSE, 6); + + priv->progress_label = gtk_label_new (NULL); + gtk_widget_show (priv->progress_label); + gtk_misc_set_alignment (GTK_MISC (priv->progress_label), 0, 0.5); + gtk_box_pack_start (GTK_BOX (priv->progress_box), priv->progress_label, FALSE, FALSE, 6); /* The buttons for actions */ + priv->button_box = gtk_hbutton_box_new (); + gtk_button_box_set_layout (GTK_BUTTON_BOX (priv->button_box), GTK_BUTTONBOX_END); + gtk_box_set_spacing (GTK_BOX (priv->button_box), 12); + gtk_widget_show (priv->button_box); + gtk_box_pack_start (GTK_BOX (vbox), priv->button_box, FALSE, FALSE, 6); } GtkWidget * @@ -477,6 +708,7 @@ itip_view_set_mode (ItipView *view, ItipViewMode mode) priv->mode = mode; set_sender_text (view); + set_buttons (view); } ItipViewMode @@ -598,7 +830,7 @@ itip_view_set_summary (ItipView *view, const char *summary) if (priv->summary) g_free (priv->summary); - priv->summary = g_strdup (summary); + priv->summary = summary ? g_strstrip (g_strdup (summary)) : NULL; set_summary_text (view); } @@ -629,7 +861,7 @@ itip_view_set_location (ItipView *view, const char *location) if (priv->location) g_free (priv->location); - priv->location = g_strdup (location); + priv->location = location ? g_strstrip (g_strdup (location)) : NULL; set_location_text (view); } @@ -647,6 +879,39 @@ itip_view_get_location (ItipView *view) return priv->location; } +/* FIXME Status and description */ +void +itip_view_set_description (ItipView *view, const char *description) +{ + ItipViewPrivate *priv; + + g_return_if_fail (view != NULL); + g_return_if_fail (ITIP_IS_VIEW (view)); + + priv = view->priv; + + if (priv->description) + g_free (priv->description); + + priv->description = description ? g_strstrip (g_strdup (description)) : NULL; + + set_description_text (view); +} + +const char * +itip_view_get_description (ItipView *view) +{ + ItipViewPrivate *priv; + + g_return_val_if_fail (view != NULL, NULL); + g_return_val_if_fail (ITIP_IS_VIEW (view), NULL); + + priv = view->priv; + + return priv->description; +} + + void itip_view_set_start (ItipView *view, struct tm *start) { @@ -718,3 +983,64 @@ itip_view_get_end (ItipView *view) return priv->end_tm; } + +void +itip_view_add_info_item (ItipView *view, ItipViewInfoItemType type, const char *message) +{ + ItipViewPrivate *priv; + ItipViewInfoItem *item; + + g_return_if_fail (view != NULL); + g_return_if_fail (ITIP_IS_VIEW (view)); + + priv = view->priv; + + item = g_new0 (ItipViewInfoItem, 1); + + item->type = type; + item->message = g_strdup (message); + + priv->info_items = g_slist_append (priv->info_items, item); + + set_info_items (view); +} + +void +itip_view_clear_info_items (ItipView *view) +{ + ItipViewPrivate *priv; + GSList *l; + + g_return_if_fail (view != NULL); + g_return_if_fail (ITIP_IS_VIEW (view)); + + priv = view->priv; + + gtk_container_foreach (GTK_CONTAINER (priv->info_box), (GtkCallback) gtk_widget_destroy, NULL); + + for (l = priv->info_items; l; l = l->next) { + ItipViewInfoItem *item = l->data; + + g_free (item->message); + g_free (item); + } +} + +void +itip_view_set_progress (ItipView *view, const char *message) +{ + ItipViewPrivate *priv; + + g_return_if_fail (view != NULL); + g_return_if_fail (ITIP_IS_VIEW (view)); + + priv = view->priv; + + if (priv->progress) + g_free (priv->progress); + + priv->progress = message ? g_strstrip (g_strdup (message)) : NULL; + + set_progress_text (view); +} + diff --git a/plugins/itip-formatter/itip-view.h b/plugins/itip-formatter/itip-view.h index ac9305bf2e..915b3286cb 100644 --- a/plugins/itip-formatter/itip-view.h +++ b/plugins/itip-formatter/itip-view.h @@ -43,10 +43,13 @@ typedef enum { ITIP_VIEW_MODE_NONE, ITIP_VIEW_MODE_PUBLISH, ITIP_VIEW_MODE_REQUEST, + ITIP_VIEW_MODE_COUNTER, + ITIP_VIEW_MODE_DECLINECOUNTER, ITIP_VIEW_MODE_ADD, ITIP_VIEW_MODE_REPLY, ITIP_VIEW_MODE_REFRESH, - ITIP_VIEW_MODE_CANCEL + ITIP_VIEW_MODE_CANCEL, + ITIP_VIEW_MODE_ERROR } ItipViewMode; typedef enum { @@ -55,19 +58,29 @@ typedef enum { ITIP_VIEW_RESPONSE_TENTATIVE, ITIP_VIEW_RESPONSE_DECLINE, ITIP_VIEW_RESPONSE_UPDATE, - ITIP_VIEW_RESPONSE_SEND + ITIP_VIEW_RESPONSE_REFRESH, + ITIP_VIEW_RESPONSE_OPEN } ItipViewResponse; -struct _ItipView -{ +typedef enum { + ITIP_VIEW_INFO_ITEM_TYPE_NONE, + ITIP_VIEW_INFO_ITEM_TYPE_INFO, + ITIP_VIEW_INFO_ITEM_TYPE_WARNING, + ITIP_VIEW_INFO_ITEM_TYPE_ERROR, + ITIP_VIEW_INFO_ITEM_TYPE_PROGRESS +} ItipViewInfoItemType; + + +struct _ItipView { GtkHBox parent_instance; ItipViewPrivate *priv; }; -struct _ItipViewClass -{ +struct _ItipViewClass { GtkHBoxClass parent_class; + + void (* response) (ItipView *view, int response); }; GType itip_view_get_type (void); @@ -91,12 +104,20 @@ const char *itip_view_get_summary (ItipView *view); void itip_view_set_location (ItipView *view, const char *location); const char *itip_view_get_location (ItipView *view); +void itip_view_set_description (ItipView *view, const char *description); +const char *itip_view_get_description (ItipView *view); + void itip_view_set_start (ItipView *view, struct tm *start); const struct tm *itip_view_get_start (ItipView *view); void itip_view_set_end (ItipView *view, struct tm *end); const struct tm *itip_view_get_end (ItipView *view); +void itip_view_add_info_item (ItipView *view, ItipViewInfoItemType, const char *message); +void itip_view_clear_info_items (ItipView *view); + +void itip_view_set_progress (ItipView *view, const char *message); + G_END_DECLS #endif diff --git a/plugins/itip-formatter/org-gnome-itip-formatter.eplug.in b/plugins/itip-formatter/org-gnome-itip-formatter.eplug.in index 901e695bd0..2f47b3f4a4 100644 --- a/plugins/itip-formatter/org-gnome-itip-formatter.eplug.in +++ b/plugins/itip-formatter/org-gnome-itip-formatter.eplug.in @@ -10,5 +10,11 @@ <item mime_type="text/calendar" flags="inline_disposition" format="format_itip"/> </group> </hook> + + <hook class="org.gnome.evolution.mail.config:1.0"> + <group target="prefs"> + <item type="page" path="90.bbdb" label="BBDB" factory="itip_formatter_page_factory"/> + </group> + </hook> </e-plugin> </e-plugin-list>
\ No newline at end of file |