/* * 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: * Miguel de Icaza (miguel@ximian.com) * Bertrand Guiheneuf (bg@aful.org) * And just about everyone else in evolution ... * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include "e-util/e-icon-factory.h" #include "e-util/e-poolv.h" #include "e-util/e-util-private.h" #include "e-util/e-util.h" #include "misc/e-selectable.h" #include "shell/e-shell.h" #include "shell/e-shell-settings.h" #include "table/e-cell-checkbox.h" #include "table/e-cell-hbox.h" #include "table/e-cell-date.h" #include "table/e-cell-size.h" #include "table/e-cell-text.h" #include "table/e-cell-toggle.h" #include "table/e-cell-tree.h" #include "table/e-cell-vbox.h" #include "table/e-table-sorting-utils.h" #include "table/e-tree-memory-callbacks.h" #include "table/e-tree-memory.h" #include "libemail-utils/mail-mt.h" #include "libemail-engine/e-mail-utils.h" #include "libemail-engine/mail-config.h" #include "libemail-engine/mail-ops.h" #include "libemail-engine/mail-tools.h" #include "mail/e-mail-label-list-store.h" #include "mail/e-mail-ui-session.h" #include "mail/em-utils.h" #include "mail/message-list.h" #if HAVE_CLUTTER #include #include #include #endif /*#define TIMEIT */ #ifdef TIMEIT #include #include #endif #ifdef G_OS_WIN32 #ifdef gmtime_r #undef gmtime_r #endif #ifdef localtime_r #undef localtime_r #endif /* The gmtime() and localtime() in Microsoft's C library are MT-safe */ #define gmtime_r(tp,tmp) (gmtime(tp)?(*(tmp)=*gmtime(tp),(tmp)):0) #define localtime_r(tp,tmp) (localtime(tp)?(*(tmp)=*localtime(tp),(tmp)):0) #endif #define d(x) #define t(x) #define MESSAGE_LIST_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), MESSAGE_LIST_TYPE, MessageListPrivate)) struct _MLSelection { GPtrArray *uids; CamelFolder *folder; }; struct _MessageListPrivate { GtkWidget *invisible; /* 4 selection */ EMailSession *session; struct _MLSelection clipboard; gboolean destroyed; gboolean thread_latest; gboolean any_row_changed; /* save state before regen list when this is set to true */ GtkTargetList *copy_target_list; GtkTargetList *paste_target_list; /* This aids in automatic message selection. */ time_t newest_read_date; const gchar *newest_read_uid; time_t oldest_unread_date; const gchar *oldest_unread_uid; #if HAVE_CLUTTER ClutterActor *search_texture; ClutterTimeline *timeline; #endif }; enum { PROP_0, PROP_COPY_TARGET_LIST, PROP_PASTE_TARGET_LIST, PROP_SESSION }; /* Forward Declarations */ static void message_list_selectable_init (ESelectableInterface *interface); G_DEFINE_TYPE_WITH_CODE ( MessageList, message_list, E_TYPE_TREE, G_IMPLEMENT_INTERFACE ( E_TYPE_SELECTABLE, message_list_selectable_init)) static struct { const gchar *target; GdkAtom atom; guint32 actions; } ml_drag_info[] = { { "x-uid-list", NULL, GDK_ACTION_MOVE|GDK_ACTION_COPY }, { "message/rfc822", NULL, GDK_ACTION_COPY }, { "text/uri-list", NULL, GDK_ACTION_COPY }, }; enum { DND_X_UID_LIST, /* x-uid-list */ DND_MESSAGE_RFC822, /* message/rfc822 */ DND_TEXT_URI_LIST /* text/uri-list */ }; /* What we send */ static GtkTargetEntry ml_drag_types[] = { { (gchar *) "x-uid-list", 0, DND_X_UID_LIST }, { (gchar *) "text/uri-list", 0, DND_TEXT_URI_LIST }, }; /* What we accept */ static GtkTargetEntry ml_drop_types[] = { { (gchar *) "x-uid-list", 0, DND_X_UID_LIST }, { (gchar *) "message/rfc822", 0, DND_MESSAGE_RFC822 }, { (gchar *) "text/uri-list", 0, DND_TEXT_URI_LIST }, }; /* * Default sizes for the ETable display * */ #define N_CHARS(x) (CHAR_WIDTH * (x)) #define COL_ICON_WIDTH (16) #define COL_ATTACH_WIDTH (16) #define COL_CHECK_BOX_WIDTH (16) #define COL_FROM_EXPANSION (24.0) #define COL_FROM_WIDTH_MIN (32) #define COL_SUBJECT_EXPANSION (30.0) #define COL_SUBJECT_WIDTH_MIN (32) #define COL_SENT_EXPANSION (24.0) #define COL_SENT_WIDTH_MIN (32) #define COL_RECEIVED_EXPANSION (20.0) #define COL_RECEIVED_WIDTH_MIN (32) #define COL_TO_EXPANSION (24.0) #define COL_TO_WIDTH_MIN (32) #define COL_SIZE_EXPANSION (6.0) #define COL_SIZE_WIDTH_MIN (32) #define COL_SENDER_EXPANSION (24.0) #define COL_SENDER_WIDTH_MIN (32) enum { NORMALISED_SUBJECT, NORMALISED_FROM, NORMALISED_TO, NORMALISED_LAST }; /* #define SMART_ADDRESS_COMPARE */ #ifdef SMART_ADDRESS_COMPARE struct _EMailAddress { ENameWestern *wname; gchar *address; }; typedef struct _EMailAddress EMailAddress; #endif /* SMART_ADDRESS_COMPARE */ static void on_cursor_activated_cmd (ETree *tree, gint row, ETreePath path, gpointer user_data); static void on_selection_changed_cmd (ETree *tree, MessageList *ml); static gint on_click (ETree *tree, gint row, ETreePath path, gint col, GdkEvent *event, MessageList *list); static gchar *filter_date (time_t date); static gchar *filter_size (gint size); /* note: @changes is owned/freed by the caller */ /*static void mail_do_regenerate_messagelist (MessageList *list, const gchar *search, const gchar *hideexpr, CamelFolderChangeInfo *changes);*/ static void mail_regen_list (MessageList *ml, const gchar *search, const gchar *hideexpr, CamelFolderChangeInfo *changes, gboolean scroll_to_cursor); static void mail_regen_cancel (MessageList *ml); static void clear_info (gchar *key, ETreePath *node, MessageList *ml); static void folder_changed (CamelFolder *folder, CamelFolderChangeInfo *info, MessageList *ml); enum { MESSAGE_SELECTED, MESSAGE_LIST_BUILT, LAST_SIGNAL }; static guint message_list_signals[LAST_SIGNAL] = {0, }; static const gchar *status_icons[] = { "mail-unread", "mail-read", "mail-replied", "mail-forward", "stock_mail-unread-multiple", "stock_mail-open-multiple" }; static const gchar *score_icons[] = { "stock_score-lowest", "stock_score-lower", "stock_score-low", "stock_score-normal", "stock_score-high", "stock_score-higher", "stock_score-highest" }; static const gchar *attachment_icons[] = { NULL, /* empty icon */ "mail-attachment", "stock_new-meeting" }; static const gchar *flagged_icons[] = { NULL, /* empty icon */ "emblem-important" }; static const gchar *followup_icons[] = { NULL, /* empty icon */ "stock_mail-flag-for-followup", "stock_mail-flag-for-followup-done" }; #ifdef SMART_ADDRESS_COMPARE static EMailAddress * e_mail_address_new (const gchar *address) { CamelInternetAddress *cia; EMailAddress *new; const gchar *name = NULL, *addr = NULL; cia = camel_internet_address_new (); if (camel_address_unformat (CAMEL_ADDRESS (cia), address) == -1) { g_object_unref (cia); return NULL; } camel_internet_address_get (cia, 0, &name, &addr); new = g_new (EMailAddress, 1); new->address = g_strdup (addr); if (name && *name) { new->wname = e_name_western_parse (name); } else { new->wname = NULL; } g_object_unref (cia); return new; } static void e_mail_address_free (EMailAddress *addr) { g_return_if_fail (addr != NULL); g_free (addr->address); if (addr->wname) e_name_western_free (addr->wname); g_free (addr); } static gint e_mail_address_compare (gconstpointer address1, gconstpointer address2) { const EMailAddress *addr1 = address1; const EMailAddress *addr2 = address2; gint retval; g_return_val_if_fail (addr1 != NULL, 1); g_return_val_if_fail (addr2 != NULL, -1); if (!addr1->wname && !addr2->wname) { /* have to compare addresses, one or both don't have names */ g_return_val_if_fail (addr1->address != NULL, 1); g_return_val_if_fail (addr2->address != NULL, -1); return g_ascii_strcasecmp (addr1->address, addr2->address); } if (!addr1->wname) return -1; if (!addr2->wname) return 1; if (!addr1->wname->last && !addr2->wname->last) { /* neither has a last name - default to address? */ /* FIXME: what do we compare next? */ g_return_val_if_fail (addr1->address != NULL, 1); g_return_val_if_fail (addr2->address != NULL, -1); return g_ascii_strcasecmp (addr1->address, addr2->address); } if (!addr1->wname->last) return -1; if (!addr2->wname->last) return 1; retval = g_ascii_strcasecmp (addr1->wname->last, addr2->wname->last); if (retval) return retval; /* last names are identical - compare first names */ if (!addr1->wname->first && !addr2->wname->first) return g_ascii_strcasecmp (addr1->address, addr2->address); if (!addr1->wname->first) return -1; if (!addr2->wname->first) return 1; retval = g_ascii_strcasecmp (addr1->wname->first, addr2->wname->first); if (retval) return retval; return g_ascii_strcasecmp (addr1->address, addr2->address); } #endif /* SMART_ADDRESS_COMPARE */ static gint address_compare (gconstpointer address1, gconstpointer address2, gpointer cmp_cache) { #ifdef SMART_ADDRESS_COMPARE EMailAddress *addr1, *addr2; #endif /* SMART_ADDRESS_COMPARE */ gint retval; g_return_val_if_fail (address1 != NULL, 1); g_return_val_if_fail (address2 != NULL, -1); #ifdef SMART_ADDRESS_COMPARE addr1 = e_mail_address_new (address1); addr2 = e_mail_address_new (address2); retval = e_mail_address_compare (addr1, addr2); e_mail_address_free (addr1); e_mail_address_free (addr2); #else retval = g_ascii_strcasecmp ((gchar *) address1, (gchar *) address2); #endif /* SMART_ADDRESS_COMPARE */ return retval; } static gchar * filter_size (gint size) { gfloat fsize; if (size < 1024) { return g_strdup_printf ("%d", size); } else { fsize = ((gfloat) size) / 1024.0; if (fsize < 1024.0) { return g_strdup_printf ("%.2f K", fsize); } else { fsize /= 1024.0; return g_strdup_printf ("%.2f M", fsize); } } } /* Gets the uid of the message displayed at a given view row */ static const gchar * get_message_uid (MessageList *message_list, ETreePath node) { CamelMessageInfo *info; g_return_val_if_fail (node != NULL, NULL); info = e_tree_memory_node_get_data (E_TREE_MEMORY (message_list->model), node); /* correct me if I'm wrong, but this should never be NULL, should it? */ g_return_val_if_fail (info != NULL, NULL); return camel_message_info_uid (info); } /* Gets the CamelMessageInfo for the message displayed at the given * view row. */ static CamelMessageInfo * get_message_info (MessageList *message_list, ETreePath node) { CamelMessageInfo *info; g_return_val_if_fail (node != NULL, NULL); info = e_tree_memory_node_get_data (E_TREE_MEMORY (message_list->model), node); g_return_val_if_fail (info != NULL, NULL); return info; } static const gchar * get_normalised_string (MessageList *message_list, CamelMessageInfo *info, gint col) { const gchar *string, *str; gchar *normalised; EPoolv *poolv; gint index; switch (col) { case COL_SUBJECT_NORM: string = camel_message_info_subject (info); index = NORMALISED_SUBJECT; break; case COL_FROM_NORM: string = camel_message_info_from (info); index = NORMALISED_FROM; break; case COL_TO_NORM: string = camel_message_info_to (info); index = NORMALISED_TO; break; default: string = NULL; index = NORMALISED_LAST; g_warning ("Should not be reached\n"); } /* slight optimisation */ if (string == NULL || string[0] == '\0') return ""; poolv = g_hash_table_lookup (message_list->normalised_hash, camel_message_info_uid (info)); if (poolv == NULL) { poolv = e_poolv_new (NORMALISED_LAST); g_hash_table_insert (message_list->normalised_hash, (gchar *) camel_message_info_uid (info), poolv); } else { str = e_poolv_get (poolv, index); if (*str) return str; } if (col == COL_SUBJECT_NORM) { EShell *shell = e_shell_get_default (); gint skip_len; const guchar *subject; gboolean found_re = TRUE; subject = (const guchar *) string; while (found_re) { found_re = em_utils_is_re_in_subject (shell, (const gchar *) subject, &skip_len) && skip_len > 0; if (found_re) subject += skip_len; /* jump over any spaces */ while (*subject && isspace ((gint) *subject)) subject++; } /* jump over any spaces */ while (*subject && isspace ((gint) *subject)) subject++; string = (const gchar *) subject; normalised = g_utf8_collate_key (string, -1); } else { /* because addresses require strings, not collate keys */ normalised = g_strdup (string); } e_poolv_set (poolv, index, normalised, TRUE); return e_poolv_get (poolv, index); } static void clear_selection (MessageList *ml, struct _MLSelection *selection) { if (selection->uids) { em_utils_uids_free (selection->uids); selection->uids = NULL; } if (selection->folder) { g_object_unref (selection->folder); selection->folder = NULL; } } static ETreePath ml_search_forward (MessageList *ml, gint start, gint end, guint32 flags, guint32 mask) { ETreePath path; gint row; CamelMessageInfo *info; ETreeTableAdapter *etta; etta = e_tree_get_table_adapter (E_TREE (ml)); for (row = start; row <= end; row++) { path = e_tree_table_adapter_node_at_row (etta, row); if (path && (info = get_message_info (ml, path)) && (camel_message_info_flags (info) & mask) == flags) return path; } return NULL; } static ETreePath ml_search_backward (MessageList *ml, gint start, gint end, guint32 flags, guint32 mask) { ETreePath path; gint row; CamelMessageInfo *info; ETreeTableAdapter *etta; etta = e_tree_get_table_adapter (E_TREE (ml)); for (row = start; row >= end; row--) { path = e_tree_table_adapter_node_at_row (etta, row); if (path && (info = get_message_info (ml, path)) && (camel_message_info_flags (info) & mask) == flags) return path; } return NULL; } static ETreePath ml_search_path (MessageList *ml, MessageListSelectDirection direction, guint32 flags, guint32 mask) { ETreePath node; gint row, count; ETreeTableAdapter *etta; etta = e_tree_get_table_adapter (E_TREE (ml)); if (ml->cursor_uid == NULL || (node = g_hash_table_lookup (ml->uid_nodemap, ml->cursor_uid)) == NULL) return NULL; row = e_tree_table_adapter_row_of_node (etta, node); if (row == -1) return NULL; count = e_table_model_row_count ((ETableModel *) etta); if ((direction & MESSAGE_LIST_SELECT_DIRECTION) == MESSAGE_LIST_SELECT_NEXT) node = ml_search_forward (ml, row + 1, count - 1, flags, mask); else node = ml_search_backward (ml, row - 1, 0, flags, mask); if (node == NULL && (direction & MESSAGE_LIST_SELECT_WRAP)) { if ((direction & MESSAGE_LIST_SELECT_DIRECTION) == MESSAGE_LIST_SELECT_NEXT) node = ml_search_forward (ml, 0, row, flags, mask); else node = ml_search_backward (ml, count - 1, row, flags, mask); } return node; } static void select_path (MessageList *ml, ETreePath path) { ETree *tree; ETreeTableAdapter *etta; ETreeSelectionModel *etsm; tree = E_TREE (ml); etta = e_tree_get_table_adapter (tree); etsm = (ETreeSelectionModel *) e_tree_get_selection_model (tree); g_free (ml->cursor_uid); ml->cursor_uid = NULL; e_tree_table_adapter_show_node (etta, path); e_tree_set_cursor (tree, path); e_tree_selection_model_select_single_path (etsm, path); } /** * message_list_select: * @message_list: a MessageList * @direction: the direction to search in * @flags: a set of flag values * @mask: a mask for comparing against @flags * * This moves the message list selection to a suitable row. @flags and * @mask combine to specify what constitutes a suitable row. @direction is * %MESSAGE_LIST_SELECT_NEXT if it should find the next matching * message, or %MESSAGE_LIST_SELECT_PREVIOUS if it should find the * previous. %MESSAGE_LIST_SELECT_WRAP is an option bit which specifies the * search should wrap. * * If no suitable row is found, the selection will be * unchanged. * * Returns %TRUE if a new message has been selected or %FALSE otherwise. **/ gboolean message_list_select (MessageList *ml, MessageListSelectDirection direction, guint32 flags, guint32 mask) { ETreePath path; path = ml_search_path (ml, direction, flags, mask); if (path) { select_path (ml, path); /* This function is usually called in response to a key * press, so grab focus if the message list is visible. */ if (gtk_widget_get_visible (GTK_WIDGET (ml))) gtk_widget_grab_focus (GTK_WIDGET (ml)); return TRUE; } else return FALSE; } /** * message_list_can_select: * @ml: * @direction: * @flags: * @mask: * * Returns true if the selection specified is possible with the current view. * * Return value: **/ gboolean message_list_can_select (MessageList *ml, MessageListSelectDirection direction, guint32 flags, guint32 mask) { return ml_search_path (ml, direction, flags, mask) != NULL; } /** * message_list_select_uid: * @message_list: * @uid: * * Selects the message with the given UID. **/ void message_list_select_uid (MessageList *message_list, const gchar *uid, gboolean with_fallback) { MessageListPrivate *priv; GHashTable *uid_nodemap; ETreePath node = NULL; g_return_if_fail (IS_MESSAGE_LIST (message_list)); priv = message_list->priv; uid_nodemap = message_list->uid_nodemap; if (message_list->folder == NULL) return; /* Try to find the requested message UID. */ if (uid != NULL) node = g_hash_table_lookup (uid_nodemap, uid); /* If we're busy or waiting to regenerate the message list, cache * the UID so we can try again when we're done. Otherwise if the * requested message UID was not found and 'with_fallback' is set, * try a couple fallbacks: * * 1) Oldest unread message in the list, by date received. * 2) Newest read message in the list, by date received. */ if (message_list->regen || message_list->regen_timeout_id) { g_free (message_list->pending_select_uid); message_list->pending_select_uid = g_strdup (uid); message_list->pending_select_fallback = with_fallback; } else if (with_fallback) { if (node == NULL && priv->oldest_unread_uid != NULL) node = g_hash_table_lookup ( uid_nodemap, priv->oldest_unread_uid); if (node == NULL && priv->newest_read_uid != NULL) node = g_hash_table_lookup ( uid_nodemap, priv->newest_read_uid); } if (node) { ETree *tree; ETreePath old_cur; tree = E_TREE (message_list); old_cur = e_tree_get_cursor (tree); /* This will emit a changed signal that we'll pick up */ e_tree_set_cursor (tree, node); if (old_cur == node) g_signal_emit ( message_list, message_list_signals[MESSAGE_SELECTED], 0, message_list->cursor_uid); } else { g_free (message_list->cursor_uid); message_list->cursor_uid = NULL; g_signal_emit ( message_list, message_list_signals[MESSAGE_SELECTED], 0, NULL); } } void message_list_select_next_thread (MessageList *ml) { ETreePath node; ETreeTableAdapter *etta; gint i, count, row; etta = e_tree_get_table_adapter (E_TREE (ml)); if (!ml->cursor_uid || (node = g_hash_table_lookup (ml->uid_nodemap, ml->cursor_uid)) == NULL) return; row = e_tree_table_adapter_row_of_node (etta, node); if (row == -1) return; count = e_table_model_row_count ((ETableModel *) etta); /* find the next node which has a root parent (i.e. toplevel node) */ for (i = row + 1; i < count - 1; i++) { node = e_tree_table_adapter_node_at_row (etta, i); if (node && e_tree_model_node_is_root (ml->model, e_tree_model_node_get_parent (ml->model, node))) { select_path (ml, node); return; } } } void message_list_select_prev_thread (MessageList *ml) { ETreePath node; ETreeTableAdapter *etta; gint i, row; gboolean skip_first; etta = e_tree_get_table_adapter (E_TREE (ml)); if (!ml->cursor_uid || (node = g_hash_table_lookup (ml->uid_nodemap, ml->cursor_uid)) == NULL) return; row = e_tree_table_adapter_row_of_node (etta, node); if (row == -1) return; /* skip first found if in the middle of the thread */ skip_first = !e_tree_model_node_is_root (ml->model, e_tree_model_node_get_parent (ml->model, node)); /* find the previous node which has a root parent (i.e. toplevel node) */ for (i = row - 1; i >= 0; i--) { node = e_tree_table_adapter_node_at_row (etta, i); if (node && e_tree_model_node_is_root (ml->model, e_tree_model_node_get_parent (ml->model, node))) { if (skip_first) { skip_first = FALSE; continue; } select_path (ml, node); return; } } } static gboolean message_list_select_all_timeout_cb (MessageList *message_list) { ESelectionModel *etsm; etsm = e_tree_get_selection_model (E_TREE (message_list)); e_selection_model_select_all (etsm); return FALSE; } /** * message_list_select_all: * @message_list: Message List widget * * Selects all messages in the message list. **/ void message_list_select_all (MessageList *message_list) { g_return_if_fail (IS_MESSAGE_LIST (message_list)); if (message_list->threaded && message_list->regen_timeout_id) { /* XXX The timeout below is added so that the execution * thread to expand all conversation threads would * have completed. The timeout 505 is just to ensure * that the value is a small delta more than the * timeout value in mail_regen_list(). */ g_timeout_add ( 55, (GSourceFunc) message_list_select_all_timeout_cb, message_list); } else /* If there is no threading, just select all immediately. */ message_list_select_all_timeout_cb (message_list); } typedef struct thread_select_info { MessageList *ml; GPtrArray *paths; } thread_select_info_t; static gboolean select_node (ETreeModel *model, ETreePath path, gpointer user_data) { thread_select_info_t *tsi = (thread_select_info_t *) user_data; g_ptr_array_add (tsi->paths, path); return FALSE; /*not done yet */ } static void select_thread (MessageList *message_list, void (*selector) (ETreePath, gpointer)) { ETree *tree; ETreeSelectionModel *etsm; thread_select_info_t tsi; tsi.ml = message_list; tsi.paths = g_ptr_array_new (); tree = E_TREE (message_list); etsm = (ETreeSelectionModel *) e_tree_get_selection_model (tree); e_tree_selected_path_foreach (tree, selector, &tsi); e_tree_selection_model_select_paths (etsm, tsi.paths); g_ptr_array_free (tsi.paths, TRUE); } static void thread_select_foreach (ETreePath path, gpointer user_data) { thread_select_info_t *tsi = (thread_select_info_t *) user_data; ETreeModel *model = tsi->ml->model; ETreePath node, last; node = path; do { last = node; node = e_tree_model_node_get_parent (model, node); } while (!e_tree_model_node_is_root (model, node)); g_ptr_array_add (tsi->paths, last); e_tree_model_node_traverse (model, last, select_node, tsi); } /** * message_list_select_thread: * @message_list: Message List widget * * Selects all messages in the current thread (based on cursor). **/ void message_list_select_thread (MessageList *message_list) { select_thread (message_list, thread_select_foreach); } static void subthread_select_foreach (ETreePath path, gpointer user_data) { thread_select_info_t *tsi = (thread_select_info_t *) user_data; ETreeModel *model = tsi->ml->model; e_tree_model_node_traverse (model, path, select_node, tsi); } /** * message_list_select_subthread: * @message_list: Message List widget * * Selects all messages in the current subthread (based on cursor). **/ void message_list_select_subthread (MessageList *message_list) { select_thread (message_list, subthread_select_foreach); } /** * message_list_invert_selection: * @message_list: Message List widget * * Invert the current selection in the message-list. **/ void message_list_invert_selection (MessageList *message_list) { ESelectionModel *etsm; etsm = e_tree_get_selection_model (E_TREE (message_list)); e_selection_model_invert_selection (etsm); } void message_list_copy (MessageList *ml, gboolean cut) { MessageListPrivate *p = ml->priv; GPtrArray *uids; clear_selection (ml, &p->clipboard); uids = message_list_get_selected (ml); if (uids->len > 0) { if (cut) { gint i; camel_folder_freeze (ml->folder); for (i = 0; i < uids->len; i++) camel_folder_set_message_flags ( ml->folder, uids->pdata[i], CAMEL_MESSAGE_SEEN | CAMEL_MESSAGE_DELETED, CAMEL_MESSAGE_SEEN | CAMEL_MESSAGE_DELETED); camel_folder_thaw (ml->folder); } p->clipboard.uids = uids; p->clipboard.folder = g_object_ref (ml->folder); gtk_selection_owner_set (p->invisible, GDK_SELECTION_CLIPBOARD, gtk_get_current_event_time ()); } else { em_utils_uids_free (uids); gtk_selection_owner_set (NULL, GDK_SELECTION_CLIPBOARD, gtk_get_current_event_time ()); } } void message_list_paste (MessageList *ml) { gtk_selection_convert (ml->priv->invisible, GDK_SELECTION_CLIPBOARD, gdk_atom_intern ("x-uid-list", FALSE), GDK_CURRENT_TIME); } /* * SimpleTableModel::col_count */ static gint ml_column_count (ETreeModel *etm, gpointer data) { return COL_LAST; } /* * SimpleTableModel::has_save_id */ static gboolean ml_has_save_id (ETreeModel *etm, gpointer data) { return TRUE; } /* * SimpleTableModel::get_save_id */ static gchar * ml_get_save_id (ETreeModel *etm, ETreePath path, gpointer data) { CamelMessageInfo *info; if (e_tree_model_node_is_root (etm, path)) return g_strdup("root"); /* Note: etable can ask for the save_id while we're clearing it, * which is the only time data should be null */ info = e_tree_memory_node_get_data (E_TREE_MEMORY (etm), path); if (info == NULL) return NULL; return g_strdup (camel_message_info_uid (info)); } /* * SimpleTableModel::has_save_id */ static gboolean ml_has_get_node_by_id (ETreeModel *etm, gpointer data) { return TRUE; } /* * SimpleTableModel::get_save_id */ static ETreePath ml_get_node_by_id (ETreeModel *etm, const gchar *save_id, gpointer data) { MessageList *ml; ml = data; if (!strcmp (save_id, "root")) return e_tree_model_get_root (etm); return g_hash_table_lookup (ml->uid_nodemap, save_id); } static gpointer ml_duplicate_value (ETreeModel *etm, gint col, gconstpointer value, gpointer data) { switch (col) { case COL_MESSAGE_STATUS: case COL_FLAGGED: case COL_SCORE: case COL_ATTACHMENT: case COL_DELETED: case COL_UNREAD: case COL_SENT: case COL_RECEIVED: case COL_SIZE: case COL_FOLLOWUP_FLAG_STATUS: case COL_FOLLOWUP_DUE_BY: return (gpointer) value; case COL_FROM: case COL_SUBJECT: case COL_TO: case COL_SENDER: case COL_RECIPIENTS: case COL_MIXED_SENDER: case COL_MIXED_RECIPIENTS: case COL_FOLLOWUP_FLAG: case COL_LOCATION: case COL_LABELS: return g_strdup (value); default: g_warning ("This shouldn't be reached\n"); } return NULL; } static void ml_free_value (ETreeModel *etm, gint col, gpointer value, gpointer data) { switch (col) { case COL_MESSAGE_STATUS: case COL_FLAGGED: case COL_SCORE: case COL_ATTACHMENT: case COL_DELETED: case COL_UNREAD: case COL_SENT: case COL_RECEIVED: case COL_SIZE: case COL_FOLLOWUP_FLAG_STATUS: case COL_FOLLOWUP_DUE_BY: case COL_FROM_NORM: case COL_SUBJECT_NORM: case COL_TO_NORM: case COL_SUBJECT_TRIMMED: case COL_COLOUR: break; case COL_FROM: case COL_SUBJECT: case COL_TO: case COL_FOLLOWUP_FLAG: case COL_LOCATION: case COL_SENDER: case COL_RECIPIENTS: case COL_MIXED_SENDER: case COL_MIXED_RECIPIENTS: case COL_LABELS: g_free (value); break; default: g_warning ("%s: This shouldn't be reached (col:%d)", G_STRFUNC, col); } } static gpointer ml_initialize_value (ETreeModel *etm, gint col, gpointer data) { switch (col) { case COL_MESSAGE_STATUS: case COL_FLAGGED: case COL_SCORE: case COL_ATTACHMENT: case COL_DELETED: case COL_UNREAD: case COL_SENT: case COL_RECEIVED: case COL_SIZE: case COL_FOLLOWUP_FLAG_STATUS: case COL_FOLLOWUP_DUE_BY: return NULL; case COL_FROM: case COL_SUBJECT: case COL_TO: case COL_FOLLOWUP_FLAG: case COL_LOCATION: case COL_SENDER: case COL_RECIPIENTS: case COL_MIXED_SENDER: case COL_MIXED_RECIPIENTS: case COL_LABELS: return g_strdup (""); default: g_warning ("This shouldn't be reached\n"); } return NULL; } static gboolean ml_value_is_empty (ETreeModel *etm, gint col, gconstpointer value, gpointer data) { switch (col) { case COL_MESSAGE_STATUS: case COL_FLAGGED: case COL_SCORE: case COL_ATTACHMENT: case COL_DELETED: case COL_UNREAD: case COL_SENT: case COL_RECEIVED: case COL_SIZE: case COL_FOLLOWUP_FLAG_STATUS: case COL_FOLLOWUP_DUE_BY: return value == NULL; case COL_FROM: case COL_SUBJECT: case COL_TO: case COL_FOLLOWUP_FLAG: case COL_LOCATION: case COL_SENDER: case COL_RECIPIENTS: case COL_MIXED_SENDER: case COL_MIXED_RECIPIENTS: case COL_LABELS: return !(value && *(gchar *) value); default: g_warning ("This shouldn't be reached\n"); return FALSE; } } static const gchar *status_map[] = { N_("Unseen"), N_("Seen"), N_("Answered"), N_("Forwarded"), N_("Multiple Unseen Messages"), N_("Multiple Messages"), }; static const gchar *score_map[] = { N_("Lowest"), N_("Lower"), N_("Low"), N_("Normal"), N_("High"), N_("Higher"), N_("Highest"), }; static gchar * ml_value_to_string (ETreeModel *etm, gint col, gconstpointer value, gpointer data) { guint i; switch (col) { case COL_MESSAGE_STATUS: i = GPOINTER_TO_UINT (value); if (i > 5) return g_strdup (""); return g_strdup (_(status_map[i])); case COL_SCORE: i = GPOINTER_TO_UINT (value) + 3; if (i > 6) i = 3; return g_strdup (_(score_map[i])); case COL_ATTACHMENT: case COL_FLAGGED: case COL_DELETED: case COL_UNREAD: case COL_FOLLOWUP_FLAG_STATUS: return g_strdup_printf ("%u", GPOINTER_TO_UINT(value)); case COL_SENT: case COL_RECEIVED: case COL_FOLLOWUP_DUE_BY: return filter_date (GPOINTER_TO_INT (value)); case COL_SIZE: return filter_size (GPOINTER_TO_INT (value)); case COL_FROM: case COL_SUBJECT: case COL_TO: case COL_FOLLOWUP_FLAG: case COL_LOCATION: case COL_SENDER: case COL_RECIPIENTS: case COL_MIXED_SENDER: case COL_MIXED_RECIPIENTS: case COL_LABELS: return g_strdup (value); default: g_warning ("This shouldn't be reached\n"); return NULL; } } static GdkPixbuf * ml_tree_icon_at (ETreeModel *etm, ETreePath path, gpointer model_data) { /* we dont really need an icon ... */ return NULL; } static void for_node_and_subtree_if_collapsed (MessageList *ml, ETreePath node, CamelMessageInfo *mi, ETreePathFunc func, gpointer data) { ETreeModel *etm = ml->model; ETreePath child; func (NULL, (ETreePath) mi, data); if (!node) return; child = e_tree_model_node_get_first_child (etm, node); if (child && !e_tree_node_is_expanded (E_TREE (ml), node)) e_tree_model_node_traverse (etm, node, func, data); } static gboolean unread_foreach (ETreeModel *etm, ETreePath node, gpointer data) { gboolean *saw_unread = data; CamelMessageInfo *info; if (!etm) info = (CamelMessageInfo *) node; else info = e_tree_memory_node_get_data ((ETreeMemory *) etm, node); g_return_val_if_fail (info != NULL, FALSE); if (!(camel_message_info_flags (info) & CAMEL_MESSAGE_SEEN)) *saw_unread = TRUE; return FALSE; } struct LatestData { gboolean sent; time_t latest; }; static gboolean latest_foreach (ETreeModel *etm, ETreePath node, gpointer data) { struct LatestData *ld = data; CamelMessageInfo *info; time_t date; if (!etm) info = (CamelMessageInfo *) node; else info = e_tree_memory_node_get_data ((ETreeMemory *) etm, node); g_return_val_if_fail (info != NULL, FALSE); date = ld->sent ? camel_message_info_date_sent (info) : camel_message_info_date_received (info); if (ld->latest == 0 || date > ld->latest) ld->latest = date; return FALSE; } static gchar * sanitize_recipients (const gchar *string) { GString *gstring; gboolean quoted = FALSE; const gchar *p; GString *recipients = g_string_new (""); gchar *single_add; gchar **name; if (!string || !*string) return (gchar *) ""; gstring = g_string_new (""); for (p = string; *p; p = g_utf8_next_char (p)) { gunichar c = g_utf8_get_char (p); if (c == '"') quoted = ~quoted; else if (c == ',' && !quoted) { single_add = g_string_free (gstring, FALSE); name = g_strsplit(single_add,"<",2); g_string_append (recipients, *name); g_string_append (recipients, ","); g_free (single_add); g_strfreev (name); gstring = g_string_new (""); continue; } g_string_append_unichar (gstring, c); } single_add = g_string_free (gstring, FALSE); name = g_strsplit(single_add,"<",2); g_string_append (recipients, *name); g_free (single_add); g_strfreev (name); return g_string_free (recipients, FALSE); } struct LabelsData { EMailLabelListStore *store; GHashTable *labels_tag2iter; }; static void add_label_if_known (struct LabelsData *ld, const gchar *tag) { GtkTreeIter label_defn; if (e_mail_label_list_store_lookup (ld->store, tag, &label_defn)) { g_hash_table_insert (ld->labels_tag2iter, /* Should be the same as the "tag" arg */ e_mail_label_list_store_get_tag (ld->store, &label_defn), gtk_tree_iter_copy (&label_defn)); } } static gboolean add_all_labels_foreach (ETreeModel *etm, ETreePath node, gpointer data) { struct LabelsData *ld = data; CamelMessageInfo *msg_info; const gchar *old_label; gchar *new_label; const CamelFlag *flag; if (!etm) msg_info = (CamelMessageInfo *) node; else msg_info = e_tree_memory_node_get_data ((ETreeMemory *) etm, node); g_return_val_if_fail (msg_info != NULL, FALSE); for (flag = camel_message_info_user_flags (msg_info); flag; flag = flag->next) add_label_if_known (ld, flag->name); old_label = camel_message_info_user_tag (msg_info, "label"); if (old_label != NULL) { /* Convert old-style labels ("") to "$Label". */ new_label = g_alloca (strlen (old_label) + 10); g_stpcpy (g_stpcpy (new_label, "$Label"), old_label); add_label_if_known (ld, new_label); } return FALSE; } static const gchar * get_trimmed_subject (CamelMessageInfo *info) { const gchar *subject; const gchar *mlist; gint mlist_len = 0; gboolean found_mlist; subject = camel_message_info_subject (info); if (!subject || !*subject) return subject; mlist = camel_message_info_mlist (info); if (mlist && *mlist) { const gchar *mlist_end; mlist_end = strchr (mlist, '@'); if (mlist_end) mlist_len = mlist_end - mlist; else mlist_len = strlen (mlist); } do { EShell *shell = e_shell_get_default (); gint skip_len; gboolean found_re = TRUE; found_mlist = FALSE; while (found_re) { found_re = FALSE; found_re = em_utils_is_re_in_subject (shell, (const gchar *) subject, &skip_len) && skip_len > 0; if (found_re) subject += skip_len; /* jump over any spaces */ while (*subject && isspace ((gint) *subject)) subject++; } if (mlist_len && *subject == '[' && !g_ascii_strncasecmp ((gchar *) subject + 1, mlist, mlist_len) && subject[1 + mlist_len] == ']') { subject += 1 + mlist_len + 1; /* jump over "[mailing-list]" */ found_mlist = TRUE; /* jump over any spaces */ while (*subject && isspace ((gint) *subject)) subject++; } } while (found_mlist); /* jump over any spaces */ while (*subject && isspace ((gint) *subject)) subject++; return subject; } static gpointer ml_tree_value_at_ex (ETreeModel *etm, ETreePath path, gint col, CamelMessageInfo *msg_info, MessageList *message_list) { EMailSession *session; const gchar *str; guint32 flags; session = message_list_get_session (message_list); g_return_val_if_fail (msg_info != NULL, NULL); switch (col) { case COL_MESSAGE_STATUS: flags = camel_message_info_flags (msg_info); if (flags & CAMEL_MESSAGE_ANSWERED) return GINT_TO_POINTER (2); else if (flags & CAMEL_MESSAGE_FORWARDED) return GINT_TO_POINTER (3); else if (flags & CAMEL_MESSAGE_SEEN) return GINT_TO_POINTER (1); else return GINT_TO_POINTER (0); case COL_FLAGGED: return GINT_TO_POINTER ((camel_message_info_flags (msg_info) & CAMEL_MESSAGE_FLAGGED) != 0); case COL_SCORE: { const gchar *tag; gint score = 0; tag = camel_message_info_user_tag(msg_info, "score"); if (tag) score = atoi (tag); return GINT_TO_POINTER (score); } case COL_FOLLOWUP_FLAG_STATUS: { const gchar *tag, *cmp; /* FIXME: this all should be methods off of message-tag-followup class, * FIXME: the tag names should be namespaced :( */ tag = camel_message_info_user_tag(msg_info, "follow-up"); cmp = camel_message_info_user_tag(msg_info, "completed-on"); if (tag && tag[0]) { if (cmp && cmp[0]) return GINT_TO_POINTER (2); else return GINT_TO_POINTER (1); } else return GINT_TO_POINTER (0); } case COL_FOLLOWUP_DUE_BY: { const gchar *tag; time_t due_by; tag = camel_message_info_user_tag(msg_info, "due-by"); if (tag && *tag) { due_by = camel_header_decode_date (tag, NULL); return GINT_TO_POINTER (due_by); } else { return GINT_TO_POINTER (0); } } case COL_FOLLOWUP_FLAG: str = camel_message_info_user_tag(msg_info, "follow-up"); return (gpointer)(str ? str : ""); case COL_ATTACHMENT: if (camel_message_info_user_flag (msg_info, "$has_cal")) return GINT_TO_POINTER (2); return GINT_TO_POINTER ((camel_message_info_flags (msg_info) & CAMEL_MESSAGE_ATTACHMENTS) != 0); case COL_FROM: str = camel_message_info_from (msg_info); return (gpointer)(str ? str : ""); case COL_FROM_NORM: return (gpointer) get_normalised_string (message_list, msg_info, col); case COL_SUBJECT: str = camel_message_info_subject (msg_info); return (gpointer)(str ? str : ""); case COL_SUBJECT_TRIMMED: str = get_trimmed_subject (msg_info); return (gpointer)(str ? str : ""); case COL_SUBJECT_NORM: return (gpointer) get_normalised_string (message_list, msg_info, col); case COL_SENT: { struct LatestData ld; ld.sent = TRUE; ld.latest = 0; for_node_and_subtree_if_collapsed (message_list, path, msg_info, latest_foreach, &ld); return GINT_TO_POINTER (ld.latest); } case COL_RECEIVED: { struct LatestData ld; ld.sent = FALSE; ld.latest = 0; for_node_and_subtree_if_collapsed (message_list, path, msg_info, latest_foreach, &ld); return GINT_TO_POINTER (ld.latest); } case COL_TO: str = camel_message_info_to (msg_info); return (gpointer)(str ? str : ""); case COL_TO_NORM: return (gpointer) get_normalised_string (message_list, msg_info, col); case COL_SIZE: return GINT_TO_POINTER (camel_message_info_size (msg_info)); case COL_DELETED: return GINT_TO_POINTER ((camel_message_info_flags (msg_info) & CAMEL_MESSAGE_DELETED) != 0); case COL_UNREAD: { gboolean saw_unread = FALSE; for_node_and_subtree_if_collapsed (message_list, path, msg_info, unread_foreach, &saw_unread); return GINT_TO_POINTER (saw_unread); } case COL_COLOUR: { const gchar *colour, *due_by, *completed, *followup; /* Priority: colour tag; label tag; important flag; due-by tag */ /* This is astonisngly poorly written code */ /* To add to the woes, what color to show when the user choose multiple labels ? Don't say that I need to have the new labels[with subject] column visible always */ colour = NULL; due_by = camel_message_info_user_tag(msg_info, "due-by"); completed = camel_message_info_user_tag(msg_info, "completed-on"); followup = camel_message_info_user_tag(msg_info, "follow-up"); if (colour == NULL) { /* Get all applicable labels. */ struct LabelsData ld; ld.store = e_mail_ui_session_get_label_store ( E_MAIL_UI_SESSION (session)); ld.labels_tag2iter = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) gtk_tree_iter_free); for_node_and_subtree_if_collapsed (message_list, path, msg_info, add_all_labels_foreach, &ld); if (g_hash_table_size (ld.labels_tag2iter) == 1) { GHashTableIter iter; GtkTreeIter *label_defn; GdkColor colour_val; gchar *colour_alloced; /* Extract the single label from the hashtable. */ g_hash_table_iter_init (&iter, ld.labels_tag2iter); g_hash_table_iter_next (&iter, NULL, (gpointer *) &label_defn); e_mail_label_list_store_get_color (ld.store, label_defn, &colour_val); /* XXX Hack to avoid returning an allocated string. */ colour_alloced = gdk_color_to_string (&colour_val); colour = g_intern_string (colour_alloced); g_free (colour_alloced); } else if (camel_message_info_flags (msg_info) & CAMEL_MESSAGE_FLAGGED) { /* FIXME: extract from the important.xpm somehow. */ colour = "#A7453E"; } else if (((followup && *followup) || (due_by && *due_by)) && !(completed && *completed)) { time_t now = time (NULL); if ((followup && *followup) || now >= camel_header_decode_date (due_by, NULL)) colour = "#A7453E"; } g_hash_table_destroy (ld.labels_tag2iter); } if (!colour) colour = camel_message_info_user_tag (msg_info, "color"); return (gpointer) colour; } case COL_LOCATION: { /* Fixme : freeing memory stuff (mem leaks) */ CamelStore *store; CamelFolder *folder; CamelService *service; const gchar *store_name; const gchar *folder_name; folder = message_list->folder; if (CAMEL_IS_VEE_FOLDER (folder)) folder = camel_vee_folder_get_location ( CAMEL_VEE_FOLDER (folder), (CamelVeeMessageInfo *) msg_info, NULL); store = camel_folder_get_parent_store (folder); folder_name = camel_folder_get_full_name (folder); service = CAMEL_SERVICE (store); store_name = camel_service_get_display_name (service); return g_strdup_printf ("%s : %s", store_name, folder_name); } case COL_MIXED_RECIPIENTS: case COL_RECIPIENTS:{ str = camel_message_info_to (msg_info); return sanitize_recipients (str); } case COL_MIXED_SENDER: case COL_SENDER:{ gchar **sender_name = NULL; str = camel_message_info_from (msg_info); if (str && str[0] != '\0') { gchar *res; sender_name = g_strsplit (str,"<",2); res = g_strdup (*sender_name); g_strfreev (sender_name); return (gpointer)(res); } else return (gpointer)(""); } case COL_LABELS:{ struct LabelsData ld; GString *result = g_string_new (""); ld.store = e_mail_ui_session_get_label_store ( E_MAIL_UI_SESSION (session)); ld.labels_tag2iter = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) gtk_tree_iter_free); for_node_and_subtree_if_collapsed (message_list, path, msg_info, add_all_labels_foreach, &ld); if (g_hash_table_size (ld.labels_tag2iter) > 0) { GHashTableIter iter; GtkTreeIter *label_defn; g_hash_table_iter_init (&iter, ld.labels_tag2iter); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &label_defn)) { gchar *label_name, *label_name_clean; if (result->len > 0) g_string_append (result, ", "); label_name = e_mail_label_list_store_get_name (ld.store, label_defn); label_name_clean = e_str_without_underscores (label_name); g_string_append (result, label_name_clean); g_free (label_name_clean); g_free (label_name); } } g_hash_table_destroy (ld.labels_tag2iter); return (gpointer) g_string_free (result, FALSE); } default: g_warning ("%s: This shouldn't be reached (col:%d)", G_STRFUNC, col); return NULL; } } static gpointer ml_tree_value_at (ETreeModel *etm, ETreePath path, gint col, gpointer model_data) { MessageList *message_list = model_data; CamelMessageInfo *msg_info; if (e_tree_model_node_is_root (etm, path)) return NULL; /* retrieve the message information array */ msg_info = e_tree_memory_node_get_data (E_TREE_MEMORY (etm), path); g_return_val_if_fail (msg_info != NULL, NULL); return ml_tree_value_at_ex (etm, path, col, msg_info, message_list); } static gpointer ml_tree_sort_value_at (ETreeModel *etm, ETreePath path, gint col, gpointer model_data) { MessageList *message_list = model_data; struct LatestData ld; if (!(col == COL_SENT || col == COL_RECEIVED)) return ml_tree_value_at (etm, path, col, model_data); if (e_tree_model_node_is_root (etm, path)) return NULL; ld.sent = (col == COL_SENT); ld.latest = 0; latest_foreach (etm, path, &ld); if (message_list->priv->thread_latest) e_tree_model_node_traverse (etm, path, latest_foreach, &ld); return GINT_TO_POINTER (ld.latest); } static void ml_tree_set_value_at (ETreeModel *etm, ETreePath path, gint col, gconstpointer val, gpointer model_data) { g_warning ("This shouldn't be reached\n"); } static gboolean ml_tree_is_cell_editable (ETreeModel *etm, ETreePath path, gint col, gpointer model_data) { return FALSE; } static gchar * filter_date (time_t date) { time_t nowdate = time (NULL); time_t yesdate; struct tm then, now, yesterday; gchar buf[26]; gboolean done = FALSE; if (date == 0) return g_strdup (_("?")); localtime_r (&date, &then); localtime_r (&nowdate, &now); if (then.tm_mday == now.tm_mday && then.tm_mon == now.tm_mon && then.tm_year == now.tm_year) { e_utf8_strftime_fix_am_pm (buf, 26, _("Today %l:%M %p"), &then); done = TRUE; } if (!done) { yesdate = nowdate - 60 * 60 * 24; localtime_r (&yesdate, &yesterday); if (then.tm_mday == yesterday.tm_mday && then.tm_mon == yesterday.tm_mon && then.tm_year == yesterday.tm_year) { e_utf8_strftime_fix_am_pm (buf, 26, _("Yesterday %l:%M %p"), &then); done = TRUE; } } if (!done) { gint i; for (i = 2; i < 7; i++) { yesdate = nowdate - 60 * 60 * 24 * i; localtime_r (&yesdate, &yesterday); if (then.tm_mday == yesterday.tm_mday && then.tm_mon == yesterday.tm_mon && then.tm_year == yesterday.tm_year) { e_utf8_strftime_fix_am_pm (buf, 26, _("%a %l:%M %p"), &then); done = TRUE; break; } } } if (!done) { if (then.tm_year == now.tm_year) { e_utf8_strftime_fix_am_pm (buf, 26, _("%b %d %l:%M %p"), &then); } else { e_utf8_strftime_fix_am_pm (buf, 26, _("%b %d %Y"), &then); } } #if 0 #ifdef CTIME_R_THREE_ARGS ctime_r (&date, buf, 26); #else ctime_r (&date, buf); #endif #endif return g_strdup (buf); } static ECell * create_composite_cell (gint col) { ECell *cell_vbox, *cell_hbox, *cell_sub, *cell_date, *cell_from, *cell_tree, *cell_attach; GSettings *settings; gchar *fixed_name = NULL; gboolean show_email; gint alt_col = (col == COL_FROM) ? COL_SENDER : COL_RECIPIENTS; gboolean same_font = FALSE; settings = g_settings_new ("org.gnome.evolution.mail"); show_email = g_settings_get_boolean (settings, "show-email"); same_font = g_settings_get_boolean (settings, "vertical-view-fonts"); g_object_unref (settings); if (!same_font) { settings = g_settings_new ("org.gnome.desktop.interface"); fixed_name = g_settings_get_string (settings, "monospace-font-name"); g_object_unref (settings); } cell_vbox = e_cell_vbox_new (); cell_hbox = e_cell_hbox_new (); /* Exclude the meeting icon. */ cell_attach = e_cell_toggle_new (attachment_icons, G_N_ELEMENTS (attachment_icons)); cell_date = e_cell_date_new (NULL, GTK_JUSTIFY_RIGHT); e_cell_date_set_format_component (E_CELL_DATE (cell_date), "mail"); g_object_set (cell_date, "bold_column", COL_UNREAD, "color_column", COL_COLOUR, NULL); cell_from = e_cell_text_new (NULL, GTK_JUSTIFY_LEFT); g_object_set (cell_from, "bold_column", COL_UNREAD, "color_column", COL_COLOUR, NULL); e_cell_hbox_append (E_CELL_HBOX (cell_hbox), cell_from, show_email ? col : alt_col, 68); e_cell_hbox_append (E_CELL_HBOX (cell_hbox), cell_attach, COL_ATTACHMENT, 5); e_cell_hbox_append (E_CELL_HBOX (cell_hbox), cell_date, COL_SENT, 27); g_object_unref (cell_from); g_object_unref (cell_attach); g_object_unref (cell_date); cell_sub = e_cell_text_new (fixed_name? fixed_name : NULL, GTK_JUSTIFY_LEFT); g_object_set (cell_sub, "color_column", COL_COLOUR, NULL); cell_tree = e_cell_tree_new (TRUE, cell_sub); e_cell_vbox_append (E_CELL_VBOX (cell_vbox), cell_hbox, COL_FROM); e_cell_vbox_append (E_CELL_VBOX (cell_vbox), cell_tree, COL_SUBJECT); g_object_unref (cell_sub); g_object_unref (cell_hbox); g_object_unref (cell_tree); g_object_set_data (G_OBJECT (cell_vbox), "cell_date", cell_date); g_object_set_data (G_OBJECT (cell_vbox), "cell_sub", cell_sub); g_object_set_data (G_OBJECT (cell_vbox), "cell_from", cell_from); g_free (fixed_name); return cell_vbox; } static void composite_cell_set_strike_col (ECell *cell, gint col) { g_object_set (g_object_get_data(G_OBJECT (cell), "cell_date"), "strikeout_column", col, NULL); g_object_set (g_object_get_data(G_OBJECT (cell), "cell_from"), "strikeout_column", col, NULL); } static ETableExtras * message_list_create_extras (void) { ETableExtras *extras; ECell *cell; extras = e_table_extras_new (); e_table_extras_add_icon_name (extras, "status", "mail-unread"); e_table_extras_add_icon_name (extras, "score", "stock_score-higher"); e_table_extras_add_icon_name (extras, "attachment", "mail-attachment"); e_table_extras_add_icon_name (extras, "flagged", "emblem-important"); e_table_extras_add_icon_name (extras, "followup", "stock_mail-flag-for-followup"); e_table_extras_add_compare (extras, "address_compare", address_compare); cell = e_cell_toggle_new ( status_icons, G_N_ELEMENTS (status_icons)); e_table_extras_add_cell (extras, "render_message_status", cell); g_object_unref (cell); cell = e_cell_toggle_new ( attachment_icons, G_N_ELEMENTS (attachment_icons)); e_table_extras_add_cell (extras, "render_attachment", cell); g_object_unref (cell); cell = e_cell_toggle_new ( flagged_icons, G_N_ELEMENTS (flagged_icons)); e_table_extras_add_cell (extras, "render_flagged", cell); g_object_unref (cell); cell = e_cell_toggle_new ( followup_icons, G_N_ELEMENTS (followup_icons)); e_table_extras_add_cell (extras, "render_flag_status", cell); g_object_unref (cell); cell = e_cell_toggle_new ( score_icons, G_N_ELEMENTS (score_icons)); e_table_extras_add_cell (extras, "render_score", cell); g_object_unref (cell); /* date cell */ cell = e_cell_date_new (NULL, GTK_JUSTIFY_LEFT); e_cell_date_set_format_component (E_CELL_DATE (cell), "mail"); g_object_set (cell, "bold_column", COL_UNREAD, "color_column", COL_COLOUR, NULL); e_table_extras_add_cell (extras, "render_date", cell); g_object_unref (cell); /* text cell */ cell = e_cell_text_new (NULL, GTK_JUSTIFY_LEFT); g_object_set (cell, "bold_column", COL_UNREAD, "color_column", COL_COLOUR, NULL); e_table_extras_add_cell (extras, "render_text", cell); g_object_unref (cell); cell = e_cell_tree_new (TRUE, cell); e_table_extras_add_cell (extras, "render_tree", cell); g_object_unref (cell); /* size cell */ cell = e_cell_size_new (NULL, GTK_JUSTIFY_RIGHT); g_object_set (cell, "bold_column", COL_UNREAD, "color_column", COL_COLOUR, NULL); e_table_extras_add_cell (extras, "render_size", cell); g_object_unref (cell); /* Composite cell for wide view */ cell = create_composite_cell (COL_FROM); e_table_extras_add_cell (extras, "render_composite_from", cell); g_object_unref (cell); cell = create_composite_cell (COL_TO); e_table_extras_add_cell (extras, "render_composite_to", cell); g_object_unref (cell); /* set proper format component for a default 'date' cell renderer */ cell = e_table_extras_get_cell (extras, "date"); e_cell_date_set_format_component (E_CELL_DATE (cell), "mail"); return extras; } static void save_tree_state (MessageList *ml) { gchar *filename; if (ml->folder == NULL || (ml->search && *ml->search)) return; filename = mail_config_folder_to_cachename (ml->folder, "et-expanded-"); e_tree_save_expanded_state (E_TREE (ml), filename); g_free (filename); ml->priv->any_row_changed = FALSE; } static void load_tree_state (MessageList *ml, xmlDoc *expand_state) { if (ml->folder == NULL) return; if (expand_state) { e_tree_load_expanded_state_xml (E_TREE (ml), expand_state); } else if (!ml->search || !*ml->search) { /* only when not searching */ gchar *filename; filename = mail_config_folder_to_cachename (ml->folder, "et-expanded-"); e_tree_load_expanded_state (E_TREE (ml), filename); g_free (filename); } ml->priv->any_row_changed = FALSE; } void message_list_save_state (MessageList *ml) { save_tree_state (ml); } static void message_list_setup_etree (MessageList *message_list, gboolean outgoing) { /* build the spec based on the folder, and possibly from a saved file */ /* otherwise, leave default */ if (message_list->folder) { gint data = 1; ETableItem *item; item = e_tree_get_item (E_TREE (message_list)); g_object_set (message_list, "uniform_row_height", TRUE, NULL); g_object_set_data (G_OBJECT (((GnomeCanvasItem *) item)->canvas), "freeze-cursor", &data); /* build based on saved file */ load_tree_state (message_list, NULL); } } static void ml_selection_get (GtkWidget *widget, GtkSelectionData *data, guint info, guint time_stamp, MessageList *ml) { struct _MLSelection *selection; selection = &ml->priv->clipboard; if (selection->uids == NULL) return; if (info & 2) { /* text/plain */ d(printf("setting text/plain selection for uids\n")); em_utils_selection_set_mailbox (data, selection->folder, selection->uids); } else { /* x-uid-list */ d(printf("setting x-uid-list selection for uids\n")); em_utils_selection_set_uidlist (data, selection->folder, selection->uids); } } static gboolean ml_selection_clear_event (GtkWidget *widget, GdkEventSelection *event, MessageList *ml) { MessageListPrivate *p = ml->priv; clear_selection (ml, &p->clipboard); return TRUE; } static void ml_selection_received (GtkWidget *widget, GtkSelectionData *selection_data, guint time, MessageList *message_list) { EMailSession *session; GdkAtom target; target = gtk_selection_data_get_target (selection_data); if (target != gdk_atom_intern ("x-uid-list", FALSE)) { d(printf("Unknown selection received by message-list\n")); return; } session = message_list_get_session (message_list); /* FIXME Not passing a GCancellable or GError here. */ em_utils_selection_get_uidlist ( selection_data, session, message_list->folder, FALSE, NULL, NULL); } static void ml_tree_drag_data_get (ETree *tree, gint row, ETreePath path, gint col, GdkDragContext *context, GtkSelectionData *data, guint info, guint time, MessageList *ml) { GPtrArray *uids; uids = message_list_get_selected (ml); if (uids->len > 0) { switch (info) { case DND_X_UID_LIST: em_utils_selection_set_uidlist (data, ml->folder, uids); break; case DND_TEXT_URI_LIST: em_utils_selection_set_urilist (data, ml->folder, uids); break; } } em_utils_uids_free (uids); } /* TODO: merge this with the folder tree stuff via empopup targets */ /* Drop handling */ struct _drop_msg { MailMsg base; GdkDragContext *context; /* Only selection->data and selection->length are valid */ GtkSelectionData *selection; CamelFolder *folder; MessageList *message_list; guint32 action; guint info; guint move : 1; guint moved : 1; guint aborted : 1; }; static gchar * ml_drop_async_desc (struct _drop_msg *m) { const gchar *full_name; full_name = camel_folder_get_full_name (m->folder); if (m->move) return g_strdup_printf(_("Moving messages into folder %s"), full_name); else return g_strdup_printf(_("Copying messages into folder %s"), full_name); } static void ml_drop_async_exec (struct _drop_msg *m, GCancellable *cancellable, GError **error) { EMailSession *session; session = message_list_get_session (m->message_list); switch (m->info) { case DND_X_UID_LIST: em_utils_selection_get_uidlist ( m->selection, session, m->folder, m->action == GDK_ACTION_MOVE, cancellable, error); break; case DND_MESSAGE_RFC822: em_utils_selection_get_message (m->selection, m->folder); break; case DND_TEXT_URI_LIST: em_utils_selection_get_urilist (m->selection, m->folder); break; } } static void ml_drop_async_done (struct _drop_msg *m) { gboolean success, delete; /* ?? */ if (m->aborted) { success = FALSE; delete = FALSE; } else { success = (m->base.error == NULL); delete = success && m->move && !m->moved; } gtk_drag_finish (m->context, success, delete, GDK_CURRENT_TIME); } static void ml_drop_async_free (struct _drop_msg *m) { g_object_unref (m->context); g_object_unref (m->folder); g_object_unref (m->message_list); gtk_selection_data_free (m->selection); } static MailMsgInfo ml_drop_async_info = { sizeof (struct _drop_msg), (MailMsgDescFunc) ml_drop_async_desc, (MailMsgExecFunc) ml_drop_async_exec, (MailMsgDoneFunc) ml_drop_async_done, (MailMsgFreeFunc) ml_drop_async_free }; static void ml_drop_action (struct _drop_msg *m) { m->move = m->action == GDK_ACTION_MOVE; mail_msg_unordered_push (m); } static void ml_tree_drag_data_received (ETree *tree, gint row, ETreePath path, gint col, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint info, guint time, MessageList *ml) { struct _drop_msg *m; if (ml->folder == NULL) return; if (gtk_selection_data_get_data (selection_data) == NULL) return; if (gtk_selection_data_get_length (selection_data) == -1) return; m = mail_msg_new (&ml_drop_async_info); m->context = g_object_ref (context); m->folder = g_object_ref (ml->folder); m->message_list = g_object_ref (ml); m->action = gdk_drag_context_get_selected_action (context); m->info = info; /* need to copy, goes away once we exit */ m->selection = gtk_selection_data_copy (selection_data); ml_drop_action (m); } struct search_child_struct { gboolean found; gconstpointer looking_for; }; static void search_child_cb (GtkWidget *widget, gpointer data) { struct search_child_struct *search = (struct search_child_struct *) data; search->found = search->found || g_direct_equal (widget, search->looking_for); } static gboolean is_tree_widget_children (ETree *tree, gconstpointer widget) { struct search_child_struct search; search.found = FALSE; search.looking_for = widget; gtk_container_foreach (GTK_CONTAINER (tree), search_child_cb, &search); return search.found; } static gboolean ml_tree_drag_motion (ETree *tree, GdkDragContext *context, gint x, gint y, guint time, MessageList *ml) { GList *targets; GdkDragAction action, actions = 0; GtkWidget *source_widget; /* If drop target is name of the account/store and not actual folder, don't allow any action */ if (!ml->folder) { gdk_drag_status (context, 0, time); return TRUE; } source_widget = gtk_drag_get_source_widget (context); /* If source widget is packed under 'tree', don't allow any action */ if (is_tree_widget_children (tree, source_widget)) { gdk_drag_status (context, 0, time); return TRUE; } if (EM_IS_FOLDER_TREE (source_widget)) { EMFolderTree *folder_tree; CamelFolder *folder = NULL; CamelStore *selected_store; gchar *selected_folder_name; gboolean has_selection; folder_tree = EM_FOLDER_TREE (source_widget); has_selection = em_folder_tree_get_selected ( folder_tree, &selected_store, &selected_folder_name); /* Sanity checks */ g_warn_if_fail ( (has_selection && selected_store != NULL) || (!has_selection && selected_store == NULL)); g_warn_if_fail ( (has_selection && selected_folder_name != NULL) || (!has_selection && selected_folder_name == NULL)); if (has_selection) { folder = camel_store_get_folder_sync ( selected_store, selected_folder_name, CAMEL_STORE_FOLDER_INFO_FAST, NULL, NULL); g_object_unref (selected_store); g_free (selected_folder_name); } if (folder == ml->folder) { gdk_drag_status (context, 0, time); return TRUE; } } targets = gdk_drag_context_list_targets (context); while (targets != NULL) { gint i; d(printf("atom drop '%s'\n", gdk_atom_name(targets->data))); for (i = 0; i < G_N_ELEMENTS (ml_drag_info); i++) if (targets->data == (gpointer) ml_drag_info[i].atom) actions |= ml_drag_info[i].actions; targets = g_list_next (targets); } d(printf("\n")); actions &= gdk_drag_context_get_actions (context); action = gdk_drag_context_get_suggested_action (context); if (action == GDK_ACTION_COPY && (actions & GDK_ACTION_MOVE)) action = GDK_ACTION_MOVE; gdk_drag_status (context, action, time); return action != 0; } static void on_model_row_changed (ETableModel *model, gint row, MessageList *ml) { ml->priv->any_row_changed = TRUE; } static gboolean ml_tree_sorting_changed (ETreeTableAdapter *adapter, MessageList *ml) { g_return_val_if_fail (ml != NULL, FALSE); if (ml->threaded && ml->frozen == 0) { if (ml->thread_tree) { /* free the previous thread_tree to recreate it fully */ camel_folder_thread_messages_unref (ml->thread_tree); ml->thread_tree = NULL; } mail_regen_list (ml, ml->search, NULL, NULL, TRUE); return TRUE; } return FALSE; } static void message_list_set_session (MessageList *message_list, EMailSession *session) { g_return_if_fail (E_IS_MAIL_SESSION (session)); g_return_if_fail (message_list->priv->session == NULL); message_list->priv->session = g_object_ref (session); } static void message_list_init (MessageList *message_list) { MessageListPrivate *p; GtkTargetList *target_list; GdkAtom matom; message_list->priv = MESSAGE_LIST_GET_PRIVATE (message_list); #if HAVE_CLUTTER message_list->priv->timeline = NULL; message_list->priv->search_texture = NULL; #endif message_list->normalised_hash = g_hash_table_new_full ( g_str_hash, g_str_equal, (GDestroyNotify) NULL, (GDestroyNotify) e_poolv_destroy); message_list->search = NULL; message_list->ensure_uid = NULL; message_list->uid_nodemap = g_hash_table_new (g_str_hash, g_str_equal); message_list->cursor_uid = NULL; message_list->last_sel_single = FALSE; message_list->regen_lock = g_mutex_new (); /* TODO: Should this only get the selection if we're realised? */ p = message_list->priv; p->invisible = gtk_invisible_new (); p->destroyed = FALSE; g_object_ref_sink (p->invisible); p->any_row_changed = FALSE; matom = gdk_atom_intern ("x-uid-list", FALSE); gtk_selection_add_target (p->invisible, GDK_SELECTION_CLIPBOARD, matom, 0); gtk_selection_add_target (p->invisible, GDK_SELECTION_CLIPBOARD, GDK_SELECTION_TYPE_STRING, 2); g_signal_connect ( p->invisible, "selection_get", G_CALLBACK (ml_selection_get), message_list); g_signal_connect ( p->invisible, "selection_clear_event", G_CALLBACK (ml_selection_clear_event), message_list); g_signal_connect ( p->invisible, "selection_received", G_CALLBACK (ml_selection_received), message_list); /* FIXME This is currently unused. */ target_list = gtk_target_list_new (NULL, 0); message_list->priv->copy_target_list = target_list; /* FIXME This is currently unused. */ target_list = gtk_target_list_new (NULL, 0); message_list->priv->paste_target_list = target_list; } static void message_list_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_SESSION: message_list_set_session ( MESSAGE_LIST (object), g_value_get_object (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void message_list_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_COPY_TARGET_LIST: g_value_set_boxed ( value, message_list_get_copy_target_list ( MESSAGE_LIST (object))); return; case PROP_PASTE_TARGET_LIST: g_value_set_boxed ( value, message_list_get_paste_target_list ( MESSAGE_LIST (object))); return; case PROP_SESSION: g_value_set_object ( value, message_list_get_session ( MESSAGE_LIST (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void message_list_dispose (GObject *object) { MessageList *message_list = MESSAGE_LIST (object); MessageListPrivate *priv; priv = message_list->priv; if (priv->session != NULL) { g_object_unref (priv->session); priv->session = NULL; } if (priv->copy_target_list != NULL) { gtk_target_list_unref (priv->copy_target_list); priv->copy_target_list = NULL; } if (priv->paste_target_list != NULL) { gtk_target_list_unref (priv->paste_target_list); priv->paste_target_list = NULL; } priv->destroyed = TRUE; if (message_list->folder) mail_regen_cancel (message_list); if (message_list->uid_nodemap) { g_hash_table_foreach (message_list->uid_nodemap, (GHFunc) clear_info, message_list); g_hash_table_destroy (message_list->uid_nodemap); message_list->uid_nodemap = NULL; } if (message_list->folder) { g_signal_handlers_disconnect_by_func ( message_list->folder, folder_changed, message_list); g_object_unref (message_list->folder); message_list->folder = NULL; } if (priv->invisible) { g_object_unref (priv->invisible); priv->invisible = NULL; } if (message_list->extras) { g_object_unref (message_list->extras); message_list->extras = NULL; } if (message_list->model) { g_object_unref (message_list->model); message_list->model = NULL; } if (message_list->idle_id != 0) { g_source_remove (message_list->idle_id); message_list->idle_id = 0; } if (message_list->seen_id) { g_source_remove (message_list->seen_id); message_list->seen_id = 0; } /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (message_list_parent_class)->dispose (object); } static void message_list_finalize (GObject *object) { MessageList *message_list = MESSAGE_LIST (object); MessageListPrivate *priv = message_list->priv; g_hash_table_destroy (message_list->normalised_hash); if (message_list->ensure_uid) { g_free (message_list->ensure_uid); message_list->ensure_uid = NULL; } if (message_list->thread_tree) camel_folder_thread_messages_unref (message_list->thread_tree); g_free (message_list->search); g_free (message_list->ensure_uid); g_free (message_list->frozen_search); g_free (message_list->cursor_uid); g_mutex_free (message_list->regen_lock); clear_selection (message_list, &priv->clipboard); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (message_list_parent_class)->finalize (object); } static void message_list_selectable_update_actions (ESelectable *selectable, EFocusTracker *focus_tracker, GdkAtom *clipboard_targets, gint n_clipboard_targets) { GtkAction *action; gboolean sensitive; action = e_focus_tracker_get_select_all_action (focus_tracker); sensitive = (e_tree_row_count (E_TREE (selectable)) > 0); gtk_action_set_tooltip (action, _("Select all visible messages")); gtk_action_set_sensitive (action, sensitive); } static void message_list_selectable_select_all (ESelectable *selectable) { message_list_select_all (MESSAGE_LIST (selectable)); } static void message_list_class_init (MessageListClass *class) { GObjectClass *object_class; gint i; for (i = 0; i < G_N_ELEMENTS (ml_drag_info); i++) ml_drag_info[i].atom = gdk_atom_intern (ml_drag_info[i].target, FALSE); g_type_class_add_private (class, sizeof (MessageListPrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = message_list_set_property; object_class->get_property = message_list_get_property; object_class->dispose = message_list_dispose; object_class->finalize = message_list_finalize; class->message_list_built = NULL; /* Inherited from ESelectableInterface */ g_object_class_override_property ( object_class, PROP_COPY_TARGET_LIST, "copy-target-list"); /* Inherited from ESelectableInterface */ g_object_class_override_property ( object_class, PROP_PASTE_TARGET_LIST, "paste-target-list"); g_object_class_install_property ( object_class, PROP_SESSION, g_param_spec_object ( "session", "Mail Session", "The mail session", E_TYPE_MAIL_SESSION, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); message_list_signals[MESSAGE_SELECTED] = g_signal_new ("message_selected", MESSAGE_LIST_TYPE, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (MessageListClass, message_selected), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); message_list_signals[MESSAGE_LIST_BUILT] = g_signal_new ("message_list_built", MESSAGE_LIST_TYPE, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (MessageListClass, message_list_built), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void message_list_selectable_init (ESelectableInterface *interface) { interface->update_actions = message_list_selectable_update_actions; interface->select_all = message_list_selectable_select_all; } static void message_list_construct (MessageList *message_list) { AtkObject *a11y; gboolean constructed; gchar *etspecfile; GSettings *settings; message_list->model = e_tree_memory_callbacks_new (ml_tree_icon_at, ml_column_count, ml_has_save_id, ml_get_save_id, ml_has_get_node_by_id, ml_get_node_by_id, ml_tree_sort_value_at, ml_tree_value_at, ml_tree_set_value_at, ml_tree_is_cell_editable, ml_duplicate_value, ml_free_value, ml_initialize_value, ml_value_is_empty, ml_value_to_string, message_list); settings = g_settings_new ("org.gnome.evolution.mail"); e_tree_memory_set_expanded_default ( E_TREE_MEMORY (message_list->model), g_settings_get_boolean (settings, "thread-expand")); message_list->priv->thread_latest = g_settings_get_boolean (settings, "thread-latest"); g_object_unref (settings); /* * The etree */ message_list->extras = message_list_create_extras (); etspecfile = g_build_filename (EVOLUTION_ETSPECDIR, "message-list.etspec", NULL); constructed = e_tree_construct_from_spec_file ( E_TREE (message_list), message_list->model, message_list->extras, etspecfile, NULL); g_free (etspecfile); if (constructed) e_tree_root_node_set_visible (E_TREE (message_list), FALSE); if (atk_get_root () != NULL) { a11y = gtk_widget_get_accessible (GTK_WIDGET (message_list)); atk_object_set_name(a11y, _("Messages")); } g_signal_connect ( e_tree_get_table_adapter (E_TREE (message_list)), "model_row_changed", G_CALLBACK (on_model_row_changed), message_list); g_signal_connect ( message_list, "cursor_activated", G_CALLBACK (on_cursor_activated_cmd), message_list); g_signal_connect ( message_list, "click", G_CALLBACK (on_click), message_list); g_signal_connect ( message_list, "selection_change", G_CALLBACK (on_selection_changed_cmd), message_list); e_tree_drag_source_set ( E_TREE (message_list), GDK_BUTTON1_MASK, ml_drag_types, G_N_ELEMENTS (ml_drag_types), GDK_ACTION_MOVE | GDK_ACTION_COPY); g_signal_connect ( message_list, "tree_drag_data_get", G_CALLBACK (ml_tree_drag_data_get), message_list); e_tree_drag_dest_set ( E_TREE (message_list), GTK_DEST_DEFAULT_ALL, ml_drop_types, G_N_ELEMENTS (ml_drop_types), GDK_ACTION_MOVE | GDK_ACTION_COPY); g_signal_connect ( message_list, "tree_drag_data_received", G_CALLBACK (ml_tree_drag_data_received), message_list); g_signal_connect ( message_list, "drag-motion", G_CALLBACK (ml_tree_drag_motion), message_list); g_signal_connect ( e_tree_get_table_adapter (E_TREE (message_list)), "sorting_changed", G_CALLBACK (ml_tree_sorting_changed), message_list); } /** * message_list_new: * * Creates a new message-list widget. * * Returns a new message-list widget. **/ GtkWidget * message_list_new (EMailSession *session) { GtkWidget *message_list; g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL); message_list = g_object_new ( message_list_get_type (), "session", session, NULL); message_list_construct (MESSAGE_LIST (message_list)); return message_list; } EMailSession * message_list_get_session (MessageList *message_list) { g_return_val_if_fail (IS_MESSAGE_LIST (message_list), NULL); return message_list->priv->session; } static void clear_info (gchar *key, ETreePath *node, MessageList *ml) { CamelMessageInfo *info; info = e_tree_memory_node_get_data ((ETreeMemory *) ml->model, node); camel_folder_free_message_info (ml->folder, info); e_tree_memory_node_set_data ((ETreeMemory *) ml->model, node, NULL); } static void clear_tree (MessageList *ml, gboolean tfree) { ETreeModel *etm = ml->model; #ifdef TIMEIT struct timeval start, end; gulong diff; printf("Clearing tree\n"); gettimeofday (&start, NULL); #endif /* we also reset the uid_rowmap since it is no longer useful/valid anyway */ if (ml->folder) g_hash_table_foreach (ml->uid_nodemap, (GHFunc) clear_info, ml); g_hash_table_destroy (ml->uid_nodemap); ml->uid_nodemap = g_hash_table_new (g_str_hash, g_str_equal); ml->priv->newest_read_date = 0; ml->priv->newest_read_uid = NULL; ml->priv->oldest_unread_date = 0; ml->priv->oldest_unread_uid = NULL; if (ml->tree_root) { /* we should be frozen already */ e_tree_memory_node_remove (E_TREE_MEMORY (etm), ml->tree_root); } ml->tree_root = e_tree_memory_node_insert (E_TREE_MEMORY (etm), NULL, 0, NULL); if (tfree) e_tree_model_rebuilt (E_TREE_MODEL (etm)); #ifdef TIMEIT gettimeofday (&end, NULL); diff = end.tv_sec * 1000 + end.tv_usec / 1000; diff -= start.tv_sec * 1000 + start.tv_usec / 1000; printf("Clearing tree took %ld.%03ld seconds\n", diff / 1000, diff % 1000); #endif } static gboolean folder_store_supports_vjunk_folder (CamelFolder *folder) { CamelStore *store; g_return_val_if_fail (folder != NULL, FALSE); store = camel_folder_get_parent_store (folder); if (!store) return FALSE; return (store->flags & (CAMEL_STORE_VJUNK | CAMEL_STORE_REAL_JUNK_FOLDER)) != 0 || CAMEL_IS_VEE_FOLDER (folder); } /* Check if the given node is selectable in the current message list, * which depends on the type of the folder (normal, junk, trash). */ static gboolean is_node_selectable (MessageList *ml, CamelMessageInfo *info) { gboolean is_junk_folder; gboolean is_trash_folder; guint32 flags; gboolean flag_junk; gboolean flag_deleted; gboolean store_has_vjunk; g_return_val_if_fail (ml != NULL, FALSE); g_return_val_if_fail (ml->folder != NULL, FALSE); g_return_val_if_fail (info != NULL, FALSE); store_has_vjunk = folder_store_supports_vjunk_folder (ml->folder); /* check folder type */ is_junk_folder = store_has_vjunk && (ml->folder->folder_flags & CAMEL_FOLDER_IS_JUNK) != 0; is_trash_folder = ml->folder->folder_flags & CAMEL_FOLDER_IS_TRASH; /* check flags set on current message */ flags = camel_message_info_flags (info); flag_junk = store_has_vjunk && (flags & CAMEL_MESSAGE_JUNK) != 0; flag_deleted = flags & CAMEL_MESSAGE_DELETED; /* perform actions depending on folder type */ if (is_junk_folder) { /* messages in a junk folder are selectable only if * the message is marked as junk and if not deleted * when hidedeleted is set */ if (flag_junk && !(flag_deleted && ml->hidedeleted)) return TRUE; } else if (is_trash_folder) { /* messages in a trash folder are selectable unless * not deleted any more */ if (flag_deleted) return TRUE; } else { /* in normal folders it depends on hidedeleted, * hidejunk and the message flags */ if (!(flag_junk && ml->hidejunk) && !(flag_deleted && ml->hidedeleted)) return TRUE; } return FALSE; } /* We try and find something that is selectable in our tree. There is * actually no assurance that we'll find something that will still be * there next time, but its probably going to work most of the time. */ static gchar * find_next_selectable (MessageList *ml) { ETreePath node; gint last; gint vrow_orig; gint vrow; ETree *et = E_TREE (ml); CamelMessageInfo *info; node = g_hash_table_lookup (ml->uid_nodemap, ml->cursor_uid); if (node == NULL) return NULL; info = get_message_info (ml, node); if (info && is_node_selectable (ml, info)) return NULL; last = e_tree_row_count (et); /* model_to_view_row etc simply dont work for sorted views. Sigh. */ vrow_orig = e_tree_row_of_node (et, node); /* We already checked this node. */ vrow = vrow_orig + 1; while (vrow < last) { node = e_tree_node_at_row (et, vrow); info = get_message_info (ml, node); if (info && is_node_selectable (ml, info)) return g_strdup (camel_message_info_uid (info)); vrow++; } /* We didn't find any undeleted entries _below_ the currently selected one * * so let's try to find one _above_ */ vrow = vrow_orig - 1; while (vrow >= 0) { node = e_tree_node_at_row (et, vrow); info = get_message_info (ml, node); if (info && is_node_selectable (ml, info)) return g_strdup (camel_message_info_uid (info)); vrow--; } return NULL; } static ETreePath * ml_uid_nodemap_insert (MessageList *message_list, CamelMessageInfo *info, ETreePath *parent_node, gint row) { ETreeMemory *tree; ETreePath *node; const gchar *uid; time_t date; guint flags; if (parent_node == NULL) parent_node = message_list->tree_root; tree = E_TREE_MEMORY (message_list->model); node = e_tree_memory_node_insert (tree, parent_node, row, info); uid = camel_message_info_uid (info); flags = camel_message_info_flags (info); date = camel_message_info_date_received (info); camel_folder_ref_message_info (message_list->folder, info); g_hash_table_insert (message_list->uid_nodemap, (gpointer) uid, node); /* Track the latest seen and unseen messages shown, used in * fallback heuristics for automatic message selection. */ if (flags & CAMEL_MESSAGE_SEEN) { if (date > message_list->priv->newest_read_date) { message_list->priv->newest_read_date = date; message_list->priv->newest_read_uid = uid; } } else { if (message_list->priv->oldest_unread_date == 0) { message_list->priv->oldest_unread_date = date; message_list->priv->oldest_unread_uid = uid; } else if (date < message_list->priv->oldest_unread_date) { message_list->priv->oldest_unread_date = date; message_list->priv->oldest_unread_uid = uid; } } return node; } static void ml_uid_nodemap_remove (MessageList *message_list, CamelMessageInfo *info) { const gchar *uid; uid = camel_message_info_uid (info); if (uid == message_list->priv->newest_read_uid) { message_list->priv->newest_read_date = 0; message_list->priv->newest_read_uid = NULL; } if (uid == message_list->priv->oldest_unread_uid) { message_list->priv->oldest_unread_date = 0; message_list->priv->oldest_unread_uid = NULL; } g_hash_table_remove (message_list->uid_nodemap, uid); camel_folder_free_message_info (message_list->folder, info); } /* only call if we have a tree model */ /* builds the tree structure */ #define BROKEN_ETREE /* avoid some broken code in etree(?) by not using the incremental update */ static void build_subtree (MessageList *ml, ETreePath parent, CamelFolderThreadNode *c, gint *row); static void build_subtree_diff (MessageList *ml, ETreePath parent, ETreePath path, CamelFolderThreadNode *c, gint *row); static void build_tree (MessageList *ml, CamelFolderThread *thread, CamelFolderChangeInfo *changes) { gint row = 0; ETreeModel *etm = ml->model; #ifndef BROKEN_ETREE ETreePath *top; #endif gchar *saveuid = NULL; #ifdef BROKEN_ETREE GPtrArray *selected; #endif #ifdef TIMEIT struct timeval start, end; gulong diff; printf("Building tree\n"); gettimeofday (&start, NULL); #endif #ifdef TIMEIT gettimeofday (&end, NULL); diff = end.tv_sec * 1000 + end.tv_usec / 1000; diff -= start.tv_sec * 1000 + start.tv_usec / 1000; printf("Loading tree state took %ld.%03ld seconds\n", diff / 1000, diff % 1000); #endif if (ml->tree_root == NULL) { ml->tree_root = e_tree_memory_node_insert (E_TREE_MEMORY (etm), NULL, 0, NULL); } if (ml->cursor_uid) saveuid = find_next_selectable (ml); #ifndef BROKEN_ETREE top = e_tree_model_node_get_first_child (etm, ml->tree_root); if (top == NULL || changes == NULL) { #else selected = message_list_get_selected (ml); #endif e_tree_memory_freeze (E_TREE_MEMORY (etm)); clear_tree (ml, FALSE); build_subtree (ml, ml->tree_root, thread->tree, &row); e_tree_memory_thaw (E_TREE_MEMORY (etm)); #ifdef BROKEN_ETREE message_list_set_selected (ml, selected); em_utils_uids_free (selected); #else } else { static gint tree_equal (ETreeModel *etm, ETreePath ap, CamelFolderThreadNode *bp); build_subtree_diff (ml, ml->tree_root, top, thread->tree, &row); top = e_tree_model_node_get_first_child (etm, ml->tree_root); tree_equal (ml->model, top, thread->tree); } #endif if (!saveuid && ml->cursor_uid && g_hash_table_lookup (ml->uid_nodemap, ml->cursor_uid)) { /* this makes sure a visible node is selected, like when * collapsing all nodes and a children had been selected */ saveuid = g_strdup (ml->cursor_uid); } if (saveuid) { ETreePath node = g_hash_table_lookup (ml->uid_nodemap, saveuid); if (node == NULL) { g_free (ml->cursor_uid); ml->cursor_uid = NULL; g_signal_emit (ml, message_list_signals[MESSAGE_SELECTED], 0, NULL); } else { ETree *tree = E_TREE (ml); ETreePath parent = node; while (parent = e_tree_model_node_get_parent (etm, parent), parent) { if (!e_tree_node_is_expanded (tree, parent)) node = parent; } e_tree_set_cursor (E_TREE (ml), node); } g_free (saveuid); } else if (ml->cursor_uid && !g_hash_table_lookup (ml->uid_nodemap, ml->cursor_uid)) { g_free (ml->cursor_uid); ml->cursor_uid = NULL; g_signal_emit (ml, message_list_signals[MESSAGE_SELECTED], 0, NULL); } #ifdef TIMEIT gettimeofday (&end, NULL); diff = end.tv_sec * 1000 + end.tv_usec / 1000; diff -= start.tv_sec * 1000 + start.tv_usec / 1000; printf("Building tree took %ld.%03ld seconds\n", diff / 1000, diff % 1000); #endif } /* this is about 20% faster than build_subtree_diff, * entirely because e_tree_model_node_insert (xx, -1 xx) * is faster than inserting to the right row :( */ /* Otherwise, this code would probably go as it does the same thing essentially */ static void build_subtree (MessageList *ml, ETreePath parent, CamelFolderThreadNode *c, gint *row) { ETreePath node; while (c) { /* phantom nodes no longer allowed */ if (!c->message) { g_warning("c->message shouldn't be NULL\n"); c = c->next; continue; } node = ml_uid_nodemap_insert ( ml, (CamelMessageInfo *) c->message, parent, -1); if (c->child) { build_subtree (ml, node, c->child, row); } c = c->next; } } /* compares a thread tree node with the etable tree node to see if they point to * the same object */ static gint node_equal (ETreeModel *etm, ETreePath ap, CamelFolderThreadNode *bp) { CamelMessageInfo *info; info = e_tree_memory_node_get_data (E_TREE_MEMORY (etm), ap); if (bp->message && strcmp (camel_message_info_uid (info), camel_message_info_uid (bp->message)) == 0) return 1; return 0; } #ifndef BROKEN_ETREE /* debug function - compare the two trees to see if they are the same */ static gint tree_equal (ETreeModel *etm, ETreePath ap, CamelFolderThreadNode *bp) { CamelMessageInfo *info; while (ap && bp) { if (!node_equal (etm, ap, bp)) { g_warning("Nodes in tree differ"); info = e_tree_memory_node_get_data (E_TREE_MEMORY (etm), ap); printf("table uid = %s\n", camel_message_info_uid(info)); printf("camel uid = %s\n", camel_message_info_uid(bp->message)); return FALSE; } else { if (!tree_equal (etm, e_tree_model_node_get_first_child (etm, ap), bp->child)) return FALSE; } bp = bp->next; ap = e_tree_model_node_get_next (etm, ap); } if (ap || bp) { g_warning("Tree differs, out of nodes in one branch"); if (ap) { info = e_tree_memory_node_get_data (E_TREE_MEMORY (etm), ap); if (info) printf("table uid = %s\n", camel_message_info_uid(info)); else printf("info is empty?\n"); } if (bp) { printf("camel uid = %s\n", camel_message_info_uid(bp->message)); return FALSE; } return FALSE; } return TRUE; } #endif /* adds a single node, retains save state, and handles adding children if required */ static void add_node_diff (MessageList *ml, ETreePath parent, ETreePath path, CamelFolderThreadNode *c, gint *row, gint myrow) { CamelMessageInfo *info; ETreePath node; g_return_if_fail (c->message != NULL); /* XXX Casting away constness. */ info = (CamelMessageInfo *) c->message; /* we just update the hashtable key */ ml_uid_nodemap_remove (ml, info); node = ml_uid_nodemap_insert (ml, info, parent, myrow); (*row)++; if (c->child) { build_subtree_diff (ml, node, NULL, c->child, row); } } /* removes node, children recursively and all associated data */ static void remove_node_diff (MessageList *ml, ETreePath node, gint depth) { ETreeModel *etm = ml->model; ETreePath cp, cn; CamelMessageInfo *info; t(printf("Removing node: %s\n", (gchar *)e_tree_memory_node_get_data(etm, node))); /* we depth-first remove all node data's ... */ cp = e_tree_model_node_get_first_child (etm, node); while (cp) { cn = e_tree_model_node_get_next (etm, cp); remove_node_diff (ml, cp, depth + 1); cp = cn; } /* and the rowid entry - if and only if it is referencing this node */ info = e_tree_memory_node_get_data (E_TREE_MEMORY (etm), node); /* and only at the toplevel, remove the node (etree should optimise this remove somewhat) */ if (depth == 0) e_tree_memory_node_remove (E_TREE_MEMORY (etm), node); g_return_if_fail (info); ml_uid_nodemap_remove (ml, info); } /* applies a new tree structure to an existing tree, but only by changing things * that have changed */ static void build_subtree_diff (MessageList *ml, ETreePath parent, ETreePath path, CamelFolderThreadNode *c, gint *row) { ETreeModel *etm = ml->model; ETreePath ap, *ai, *at, *tmp; CamelFolderThreadNode *bp, *bi, *bt; gint i, j, myrow = 0; ap = path; bp = c; while (ap || bp) { t(printf("Processing row: %d (subtree row %d)\n", *row, myrow)); if (ap == NULL) { t(printf("out of old nodes\n")); /* ran out of old nodes - remaining nodes are added */ add_node_diff (ml, parent, ap, bp, row, myrow); myrow++; bp = bp->next; } else if (bp == NULL) { t(printf("out of new nodes\n")); /* ran out of new nodes - remaining nodes are removed */ tmp = e_tree_model_node_get_next (etm, ap); remove_node_diff (ml, ap, 0); ap = tmp; } else if (node_equal (etm, ap, bp)) { /*t(printf("nodes match, verify\n"));*/ /* matching nodes, verify details/children */ #if 0 if (bp->message) { gpointer olduid, oldrow; /* if this is a message row, check/update the row id map */ if (g_hash_table_lookup_extended (ml->uid_rowmap, camel_message_info_uid (bp->message), &olduid, &oldrow)) { if ((gint) oldrow != (*row)) { g_hash_table_insert (ml->uid_rowmap, olduid, (gpointer)(*row)); } } else { g_warning("Cannot find uid %s in table?", camel_message_info_uid(bp->message)); } } #endif *row = (*row)+1; myrow++; tmp = e_tree_model_node_get_first_child (etm, ap); /* make child lists match (if either has one) */ if (bp->child || tmp) { build_subtree_diff (ml, ap, tmp, bp->child, row); } ap = e_tree_model_node_get_next (etm, ap); bp = bp->next; } else { t(printf("searching for matches\n")); /* we have to scan each side for a match */ bi = bp->next; ai = e_tree_model_node_get_next (etm, ap); for (i = 1; bi != NULL; i++,bi = bi->next) { if (node_equal (etm, ap, bi)) break; } for (j = 1; ai != NULL; j++,ai = e_tree_model_node_get_next (etm, ai)) { if (node_equal (etm, ai, bp)) break; } if (i < j) { /* smaller run of new nodes - must be nodes to add */ if (bi) { bt = bp; while (bt != bi) { t(printf("adding new node 0\n")); add_node_diff (ml, parent, NULL, bt, row, myrow); myrow++; bt = bt->next; } bp = bi; } else { t(printf("adding new node 1\n")); /* no match in new nodes, add one, try next */ add_node_diff (ml, parent, NULL, bp, row, myrow); myrow++; bp = bp->next; } } else { /* bigger run of old nodes - must be nodes to remove */ if (ai) { at = ap; while (at != ai) { t(printf("removing old node 0\n")); tmp = e_tree_model_node_get_next (etm, at); remove_node_diff (ml, at, 0); at = tmp; } ap = ai; } else { t(printf("adding new node 2\n")); /* didn't find match in old nodes, must be new node? */ add_node_diff (ml, parent, NULL, bp, row, myrow); myrow++; bp = bp->next; #if 0 tmp = e_tree_model_node_get_next (etm, ap); remove_node_diff (etm, ap, 0); ap = tmp; #endif } } } } } #ifndef BROKEN_ETREE static void build_flat_diff (MessageList *ml, CamelFolderChangeInfo *changes); #endif static void build_flat (MessageList *ml, GPtrArray *summary, CamelFolderChangeInfo *changes) { ETreeModel *etm = ml->model; gchar *saveuid = NULL; gint i; #ifdef BROKEN_ETREE GPtrArray *selected; #endif #ifdef TIMEIT struct timeval start, end; gulong diff; printf("Building flat\n"); gettimeofday (&start, NULL); #endif if (ml->cursor_uid) saveuid = find_next_selectable (ml); #ifndef BROKEN_ETREE if (changes) { build_flat_diff (ml, changes); } else { #else selected = message_list_get_selected (ml); #endif e_tree_memory_freeze (E_TREE_MEMORY (etm)); clear_tree (ml, FALSE); for (i = 0; i < summary->len; i++) { CamelMessageInfo *info = summary->pdata[i]; ml_uid_nodemap_insert (ml, info, NULL, -1); } e_tree_memory_thaw (E_TREE_MEMORY (etm)); #ifdef BROKEN_ETREE message_list_set_selected (ml, selected); em_utils_uids_free (selected); #else } #endif if (saveuid) { ETreePath node = g_hash_table_lookup (ml->uid_nodemap, saveuid); if (node == NULL) { g_free (ml->cursor_uid); ml->cursor_uid = NULL; g_signal_emit (ml, message_list_signals[MESSAGE_SELECTED], 0, NULL); } else { e_tree_set_cursor (E_TREE (ml), node); } g_free (saveuid); } #ifdef TIMEIT gettimeofday (&end, NULL); diff = end.tv_sec * 1000 + end.tv_usec / 1000; diff -= start.tv_sec * 1000 + start.tv_usec / 1000; printf("Building flat took %ld.%03ld seconds\n", diff / 1000, diff % 1000); #endif } static void message_list_change_first_visible_parent (MessageList *ml, ETreePath node) { ETreePath first_visible = NULL; while (node && (node = e_tree_model_node_get_parent (ml->model, node))) { if (!e_tree_node_is_expanded (E_TREE (ml), node)) first_visible = node; } if (first_visible != NULL) { e_tree_model_pre_change (ml->model); e_tree_model_node_data_changed (ml->model, first_visible); } } #ifndef BROKEN_ETREE static void build_flat_diff (MessageList *ml, CamelFolderChangeInfo *changes) { gint i; ETreePath node; CamelMessageInfo *info; #ifdef TIMEIT struct timeval start, end; gulong diff; gettimeofday (&start, NULL); #endif d(printf("updating changes to display\n")); /* remove individual nodes? */ d(printf("Removing messages from view:\n")); for (i = 0; i < changes->uid_removed->len; i++) { node = g_hash_table_lookup (ml->uid_nodemap, changes->uid_removed->pdata[i]); if (node) { info = e_tree_memory_node_get_data (E_TREE_MEMORY (ml->model), node); e_tree_memory_node_remove (E_TREE_MEMORY (ml->model), node); ml_uid_nodemap_remove (ml, info); } } /* add new nodes? - just append to the end */ d(printf("Adding messages to view:\n")); for (i = 0; i < changes->uid_added->len; i++) { info = camel_folder_get_message_info (ml->folder, changes->uid_added->pdata[i]); if (info) { d(printf(" %s\n", (gchar *)changes->uid_added->pdata[i])); ml_uid_nodemap_insert (ml, info, NULL, -1); } } /* and update changes too */ d(printf("Changing messages to view:\n")); for (i = 0; i < changes->uid_changed->len; i++) { ETreePath *node = g_hash_table_lookup (ml->uid_nodemap, changes->uid_changed->pdata[i]); if (node) { e_tree_model_pre_change (ml->model); e_tree_model_node_data_changed (ml->model, node); message_list_change_first_visible_parent (ml, node); } } #ifdef TIMEIT gettimeofday (&end, NULL); diff = end.tv_sec * 1000 + end.tv_usec / 1000; diff -= start.tv_sec * 1000 + start.tv_usec / 1000; printf("Inserting changes took %ld.%03ld seconds\n", diff / 1000, diff % 1000); #endif } #endif /* BROKEN_ETREE */ static CamelFolderChangeInfo * mail_folder_hide_by_flag (CamelFolder *folder, MessageList *ml, CamelFolderChangeInfo *changes, gint flag) { CamelFolderChangeInfo *newchanges; CamelMessageInfo *info; gint i; newchanges = camel_folder_change_info_new (); for (i = 0; i < changes->uid_changed->len; i++) { ETreePath node; guint32 flags; node = g_hash_table_lookup ( ml->uid_nodemap, changes->uid_changed->pdata[i]); info = camel_folder_get_message_info ( folder, changes->uid_changed->pdata[i]); if (info) flags = camel_message_info_flags (info); if (node != NULL && info != NULL && (flags & flag) != 0) camel_folder_change_info_remove_uid ( newchanges, changes->uid_changed->pdata[i]); else if (node == NULL && info != NULL && (flags & flag) == 0) camel_folder_change_info_add_uid ( newchanges, changes->uid_changed->pdata[i]); else camel_folder_change_info_change_uid ( newchanges, changes->uid_changed->pdata[i]); if (info) camel_folder_free_message_info (folder, info); } if (newchanges->uid_added->len > 0 || newchanges->uid_removed->len > 0) { for (i = 0; i < changes->uid_added->len; i++) camel_folder_change_info_add_uid ( newchanges, changes->uid_added->pdata[i]); for (i = 0; i < changes->uid_removed->len; i++) camel_folder_change_info_remove_uid ( newchanges, changes->uid_removed->pdata[i]); } else { camel_folder_change_info_clear (newchanges); camel_folder_change_info_cat (newchanges, changes); } return newchanges; } static void folder_changed (CamelFolder *folder, CamelFolderChangeInfo *changes, MessageList *ml) { CamelFolderChangeInfo *altered_changes = NULL; gint i; if (ml->priv->destroyed) return; d(printf("folder changed event, changes = %p\n", changes)); if (changes) { d(printf("changed = %d added = %d removed = %d\n", changes->uid_changed->len, changes->uid_added->len, changes->uid_removed->len)); for (i = 0; i < changes->uid_removed->len; i++) g_hash_table_remove ( ml->normalised_hash, changes->uid_removed->pdata[i]); /* check if the hidden state has changed, if so modify accordingly, then regenerate */ if (ml->hidejunk || ml->hidedeleted) altered_changes = mail_folder_hide_by_flag ( folder, ml, changes, (ml->hidejunk ? CAMEL_MESSAGE_JUNK : 0) | (ml->hidedeleted ? CAMEL_MESSAGE_DELETED : 0)); else { altered_changes = camel_folder_change_info_new (); camel_folder_change_info_cat (altered_changes, changes); } if (altered_changes->uid_added->len == 0 && altered_changes->uid_removed->len == 0 && altered_changes->uid_changed->len < 100) { for (i = 0; i < altered_changes->uid_changed->len; i++) { ETreePath node = g_hash_table_lookup (ml->uid_nodemap, altered_changes->uid_changed->pdata[i]); if (node) { e_tree_model_pre_change (ml->model); e_tree_model_node_data_changed (ml->model, node); message_list_change_first_visible_parent (ml, node); } } camel_folder_change_info_free (altered_changes); g_signal_emit (ml, message_list_signals[MESSAGE_LIST_BUILT], 0); return; } } /* XXX This apparently eats the ChangeFolderChangeInfo. */ mail_regen_list (ml, ml->search, NULL, altered_changes, FALSE); } /** * message_list_set_folder: * @message_list: Message List widget * @folder: folder backend to be set * @outgoing: whether this is an outgoing folder * * Sets @folder to be the backend folder for @message_list. If * @outgoing is %TRUE, then the message-list UI changes to default to * the "Outgoing folder" column view. **/ void message_list_set_folder (MessageList *message_list, CamelFolder *folder, gboolean outgoing) { ETreeModel *etm = message_list->model; gboolean hide_deleted; GSettings *settings; g_return_if_fail (IS_MESSAGE_LIST (message_list)); if (message_list->folder == folder) return; g_free (message_list->search); message_list->search = NULL; g_free (message_list->frozen_search); message_list->frozen_search = NULL; if (message_list->seen_id) { g_source_remove (message_list->seen_id); message_list->seen_id = 0; } /* reset the normalised sort performance hack */ g_hash_table_remove_all (message_list->normalised_hash); mail_regen_cancel (message_list); if (message_list->folder != NULL) { save_tree_state (message_list); } e_tree_memory_freeze (E_TREE_MEMORY (etm)); clear_tree (message_list, TRUE); e_tree_memory_thaw (E_TREE_MEMORY (etm)); /* remove the cursor activate idle handler */ if (message_list->idle_id != 0) { g_source_remove (message_list->idle_id); message_list->idle_id = 0; } if (message_list->folder) { g_signal_handlers_disconnect_by_func ( message_list->folder, folder_changed, message_list); if (message_list->uid_nodemap) g_hash_table_foreach (message_list->uid_nodemap, (GHFunc) clear_info, message_list); g_object_unref (message_list->folder); message_list->folder = NULL; } if (message_list->thread_tree) { camel_folder_thread_messages_unref (message_list->thread_tree); message_list->thread_tree = NULL; } if (message_list->cursor_uid) { g_free (message_list->cursor_uid); message_list->cursor_uid = NULL; g_signal_emit (message_list, message_list_signals[MESSAGE_SELECTED], 0, NULL); } if (CAMEL_IS_FOLDER (folder)) { CamelStore *store; gboolean non_trash_folder; gint strikeout_col; ECell *cell; message_list->folder = g_object_ref (folder); message_list->just_set_folder = TRUE; store = camel_folder_get_parent_store (folder); non_trash_folder = ((store->flags & CAMEL_STORE_VTRASH) == 0) || ((folder->folder_flags & CAMEL_FOLDER_IS_TRASH) == 0); /* Setup the strikeout effect for non-trash folders */ strikeout_col = non_trash_folder ? COL_DELETED : -1; cell = e_table_extras_get_cell (message_list->extras, "render_date"); g_object_set (cell, "strikeout_column", strikeout_col, NULL); cell = e_table_extras_get_cell (message_list->extras, "render_text"); g_object_set (cell, "strikeout_column", strikeout_col, NULL); cell = e_table_extras_get_cell (message_list->extras, "render_size"); g_object_set (cell, "strikeout_column", strikeout_col, NULL); cell = e_table_extras_get_cell (message_list->extras, "render_composite_from"); composite_cell_set_strike_col (cell, strikeout_col); cell = e_table_extras_get_cell (message_list->extras, "render_composite_to"); composite_cell_set_strike_col (cell, strikeout_col); /* Build the etree suitable for this folder */ message_list_setup_etree (message_list, outgoing); g_signal_connect ( folder, "changed", G_CALLBACK (folder_changed), message_list); settings = g_settings_new ("org.gnome.evolution.mail"); hide_deleted = !g_settings_get_boolean (settings, "show-deleted"); g_object_unref (settings); message_list->hidedeleted = hide_deleted && non_trash_folder; message_list->hidejunk = folder_store_supports_vjunk_folder (message_list->folder) && !(folder->folder_flags & CAMEL_FOLDER_IS_JUNK) && !(folder->folder_flags & CAMEL_FOLDER_IS_TRASH); if (message_list->frozen == 0) mail_regen_list (message_list, message_list->search, NULL, NULL, TRUE); } } GtkTargetList * message_list_get_copy_target_list (MessageList *message_list) { g_return_val_if_fail (IS_MESSAGE_LIST (message_list), NULL); return message_list->priv->copy_target_list; } GtkTargetList * message_list_get_paste_target_list (MessageList *message_list) { g_return_val_if_fail (IS_MESSAGE_LIST (message_list), NULL); return message_list->priv->paste_target_list; } static gboolean on_cursor_activated_idle (gpointer data) { MessageList *message_list = data; ESelectionModel *esm; gint selected; esm = e_tree_get_selection_model (E_TREE (message_list)); selected = e_selection_model_selected_count (esm); if (selected == 1 && message_list->cursor_uid) { d(printf ("emitting cursor changed signal, for uid %s\n", message_list->cursor_uid)); g_signal_emit (message_list, message_list_signals[MESSAGE_SELECTED], 0, message_list->cursor_uid); } else { g_signal_emit (message_list, message_list_signals[MESSAGE_SELECTED], 0, NULL); } message_list->idle_id = 0; return FALSE; } static void on_cursor_activated_cmd (ETree *tree, gint row, ETreePath path, gpointer user_data) { MessageList *message_list = MESSAGE_LIST (user_data); const gchar *new_uid; if (path == NULL) new_uid = NULL; else new_uid = get_message_uid (message_list, path); /* Do not check the cursor_uid and the new_uid values, because the * selected item (set in on_selection_changed_cmd) can be different * from the one with a cursor (when selecting with Ctrl, for example). * This has a little side-effect, when keeping list it that state, * then changing folders forth and back will select and move cursor * to that selected item. Does anybody consider it as a bug? */ if ((message_list->cursor_uid == NULL && new_uid == NULL) || (message_list->last_sel_single && message_list->cursor_uid != NULL && new_uid != NULL)) return; g_free (message_list->cursor_uid); message_list->cursor_uid = g_strdup (new_uid); if (!message_list->idle_id) { message_list->idle_id = g_idle_add_full (G_PRIORITY_LOW, on_cursor_activated_idle, message_list, NULL); } } static void on_selection_changed_cmd (ETree *tree, MessageList *ml) { GPtrArray *uids; const gchar *newuid; ETreePath cursor; /* not sure if we could just ignore this for the cursor, i think sometimes you * only get a selection changed when you should also get a cursor activated? */ uids = message_list_get_selected (ml); if (uids->len == 1) newuid = g_ptr_array_index (uids, 0); else if ((cursor = e_tree_get_cursor (tree))) newuid = (gchar *) camel_message_info_uid (e_tree_memory_node_get_data ((ETreeMemory *) tree, cursor)); else newuid = NULL; /* If the selection isn't empty, then we ignore the no-uid check, since this event * is also used for other updating. If it is empty, it might just be a setup event * from etree which we do need to ignore */ if ((newuid == NULL && ml->cursor_uid == NULL && uids->len == 0) || (ml->last_sel_single && uids->len == 1 && newuid != NULL && ml->cursor_uid != NULL && !strcmp (ml->cursor_uid, newuid))) { /* noop */ } else { g_free (ml->cursor_uid); ml->cursor_uid = g_strdup (newuid); if (!ml->idle_id) ml->idle_id = g_idle_add_full (G_PRIORITY_LOW, on_cursor_activated_idle, ml, NULL); } ml->last_sel_single = uids->len == 1; em_utils_uids_free (uids); } static gint on_click (ETree *tree, gint row, ETreePath path, gint col, GdkEvent *event, MessageList *list) { CamelMessageInfo *info; gboolean folder_is_trash; const gchar *uid; gint flag = 0; guint32 flags; if (col == COL_MESSAGE_STATUS) flag = CAMEL_MESSAGE_SEEN; else if (col == COL_FLAGGED) flag = CAMEL_MESSAGE_FLAGGED; else if (col != COL_FOLLOWUP_FLAG_STATUS) return FALSE; if (!(info = get_message_info (list, path))) return FALSE; if (col == COL_FOLLOWUP_FLAG_STATUS) { const gchar *tag, *cmp; tag = camel_message_info_user_tag (info, "follow-up"); cmp = camel_message_info_user_tag (info, "completed-on"); if (tag && tag[0]) { if (cmp && cmp[0]) { camel_message_info_set_user_tag (info, "follow-up", NULL); camel_message_info_set_user_tag (info, "due-by", NULL); camel_message_info_set_user_tag (info, "completed-on", NULL); } else { gchar *text; text = camel_header_format_date (time (NULL), 0); camel_message_info_set_user_tag (info, "completed-on", text); g_free (text); } } else { /* default follow-up flag name to use when clicked in the message list column */ camel_message_info_set_user_tag (info, "follow-up", _("Follow-up")); camel_message_info_set_user_tag (info, "completed-on", NULL); } return TRUE; } flags = camel_message_info_flags (info); folder_is_trash = ((list->folder->folder_flags & CAMEL_FOLDER_IS_TRASH) != 0); /* If a message was marked as deleted and the user flags it as * important or unread in a non-Trash folder, then undelete the * message. We avoid automatically undeleting messages while * viewing a Trash folder because it would cause the message to * suddenly disappear from the message list, which is confusing * and alarming to the user. */ if (!folder_is_trash && flags & CAMEL_MESSAGE_DELETED) { if (col == COL_FLAGGED && !(flags & CAMEL_MESSAGE_FLAGGED)) flag |= CAMEL_MESSAGE_DELETED; if (col == COL_MESSAGE_STATUS && (flags & CAMEL_MESSAGE_SEEN)) flag |= CAMEL_MESSAGE_DELETED; } uid = camel_message_info_uid (info); camel_folder_set_message_flags (list->folder, uid, flag, ~flags); /* Notify the folder tree model that the user has marked a message * as unread so it doesn't mistake the event as new mail arriving. */ if (col == COL_MESSAGE_STATUS && (flags & CAMEL_MESSAGE_SEEN)) { EMFolderTreeModel *model; model = em_folder_tree_model_get_default (); em_folder_tree_model_user_marked_unread ( model, list->folder, 1); } if (flag == CAMEL_MESSAGE_SEEN && list->seen_id) { g_source_remove (list->seen_id); list->seen_id = 0; } return TRUE; } struct _ml_selected_data { MessageList *ml; GPtrArray *uids; }; static void ml_getselected_cb (ETreePath path, gpointer user_data) { struct _ml_selected_data *data = user_data; const gchar *uid; if (e_tree_model_node_is_root (data->ml->model, path)) return; uid = get_message_uid (data->ml, path); g_return_if_fail (uid != NULL); g_ptr_array_add (data->uids, g_strdup (uid)); } GPtrArray * message_list_get_uids (MessageList *ml) { struct _ml_selected_data data = { ml, g_ptr_array_new () }; e_tree_path_foreach (E_TREE (ml), ml_getselected_cb, &data); if (ml->folder && data.uids->len) camel_folder_sort_uids (ml->folder, data.uids); return data.uids; } GPtrArray * message_list_get_selected (MessageList *ml) { struct _ml_selected_data data = { ml, g_ptr_array_new () }; e_tree_selected_path_foreach (E_TREE (ml), ml_getselected_cb, &data); if (ml->folder && data.uids->len) camel_folder_sort_uids (ml->folder, data.uids); return data.uids; } void message_list_set_selected (MessageList *ml, GPtrArray *uids) { gint i; ETreeSelectionModel *etsm; ETreePath node; GPtrArray *paths = g_ptr_array_new (); etsm = (ETreeSelectionModel *) e_tree_get_selection_model (E_TREE (ml)); for (i = 0; i < uids->len; i++) { node = g_hash_table_lookup (ml->uid_nodemap, uids->pdata[i]); if (node) g_ptr_array_add (paths, node); } e_tree_selection_model_select_paths (etsm, paths); g_ptr_array_free (paths, TRUE); } struct ml_count_data { MessageList *ml; guint count; }; static void ml_getcount_cb (ETreePath path, gpointer user_data) { struct ml_count_data *data = user_data; if (!e_tree_model_node_is_root (data->ml->model, path)) data->count++; } guint message_list_count (MessageList *message_list) { struct ml_count_data data = { message_list, 0 }; g_return_val_if_fail (message_list != NULL, 0); g_return_val_if_fail (IS_MESSAGE_LIST (message_list), 0); e_tree_path_foreach (E_TREE (message_list), ml_getcount_cb, &data); return data.count; } static void ml_getselcount_cb (gint model_row, gpointer user_data) { struct ml_count_data *data = user_data; data->count++; } guint message_list_selected_count (MessageList *message_list) { struct ml_count_data data = { message_list, 0 }; g_return_val_if_fail (message_list != NULL, 0); g_return_val_if_fail (IS_MESSAGE_LIST (message_list), 0); e_tree_selected_row_foreach (E_TREE (message_list), ml_getselcount_cb, &data); return data.count; } void message_list_freeze (MessageList *ml) { ml->frozen++; } void message_list_thaw (MessageList *ml) { g_return_if_fail (ml->frozen != 0); ml->frozen--; if (ml->frozen == 0) { mail_regen_list (ml, ml->frozen_search ? ml->frozen_search : ml->search, NULL, NULL, TRUE); g_free (ml->frozen_search); ml->frozen_search = NULL; } } /* set whether we are in threaded view or flat view */ void message_list_set_threaded_expand_all (MessageList *ml) { if (ml->threaded) { ml->expand_all = 1; if (ml->frozen == 0) mail_regen_list (ml, ml->search, NULL, NULL, TRUE); } } void message_list_set_threaded_collapse_all (MessageList *ml) { if (ml->threaded) { ml->collapse_all = 1; if (ml->frozen == 0) mail_regen_list (ml, ml->search, NULL, NULL, TRUE); } } void message_list_set_threaded (MessageList *ml, gboolean threaded) { if (ml->threaded != threaded) { ml->threaded = threaded; if (ml->frozen == 0) mail_regen_list (ml, ml->search, NULL, NULL, TRUE); } } void message_list_set_hidedeleted (MessageList *ml, gboolean hidedeleted) { if (ml->hidedeleted != hidedeleted) { ml->hidedeleted = hidedeleted; if (ml->frozen == 0) mail_regen_list (ml, ml->search, NULL, NULL, TRUE); } } void message_list_set_search (MessageList *ml, const gchar *search) { #if HAVE_CLUTTER if (ml->priv->timeline == NULL) { ClutterActor *stage = g_object_get_data ((GObject *)ml, "stage"); if (stage) { ClutterActor *texture = NULL; ClutterPath *path; ClutterBehaviour *behaviour; ClutterAlpha *alpha; GtkIconInfo *info; info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (), "system-search", 72, GTK_ICON_LOOKUP_NO_SVG); if (info) { texture = clutter_texture_new_from_file (gtk_icon_info_get_filename (info), NULL); gtk_icon_info_free (info); } clutter_container_add_actor ((ClutterContainer *) stage, texture); ml->priv->search_texture = texture; ml->priv->timeline = clutter_timeline_new (2 * 1000); alpha = clutter_alpha_new_full (ml->priv->timeline, CLUTTER_LINEAR); path = clutter_path_new (); behaviour = clutter_behaviour_path_new (alpha, path); clutter_actor_hide (texture); clutter_path_clear (path); clutter_path_add_move_to (path, 100, 50); clutter_path_add_line_to (path, 200, 50); clutter_path_add_line_to (path, 200, 100); clutter_path_add_line_to (path, 100, 100); clutter_path_add_line_to (path, 100, 50); clutter_behaviour_apply (behaviour, texture); clutter_timeline_set_loop (ml->priv->timeline, TRUE); g_signal_connect_swapped ( ml->priv->timeline, "started", G_CALLBACK (clutter_actor_show), texture); g_signal_connect ( ml->priv->timeline, "paused", G_CALLBACK (clutter_actor_hide), texture); clutter_timeline_pause (ml->priv->timeline); clutter_timeline_stop (ml->priv->timeline); } } #endif if (search == NULL || search[0] == '\0') if (ml->search == NULL || ml->search[0] == '\0') return; if (search != NULL && ml->search != NULL && strcmp (search, ml->search) == 0) return; if (ml->thread_tree) { camel_folder_thread_messages_unref (ml->thread_tree); ml->thread_tree = NULL; } #if HAVE_CLUTTER if (ml->priv->timeline) clutter_timeline_start (ml->priv->timeline); #endif if (ml->frozen == 0) mail_regen_list (ml, search, NULL, NULL, TRUE); else { g_free (ml->frozen_search); ml->frozen_search = g_strdup (search); } } /* will ensure that the message with UID uid will be in the message list after the next rebuild */ void message_list_ensure_message (MessageList *ml, const gchar *uid) { g_return_if_fail (ml != NULL); g_free (ml->ensure_uid); ml->ensure_uid = g_strdup (uid); } struct sort_column_data { ETableCol *col; gboolean ascending; }; struct sort_message_info_data { CamelMessageInfo *mi; GPtrArray *values; /* read values so far, in order of sort_array_data::sort_columns */ }; struct sort_array_data { MessageList *ml; CamelFolder *folder; GPtrArray *sort_columns; /* struct sort_column_data in order of sorting */ GHashTable *message_infos; /* uid -> struct sort_message_info_data */ gpointer cmp_cache; GCancellable *cancellable; }; static gint cmp_array_uids (gconstpointer a, gconstpointer b, gpointer user_data) { const gchar *uid1 = *(const gchar **) a; const gchar *uid2 = *(const gchar **) b; struct sort_array_data *sort_data = user_data; gint i, res = 0; struct sort_message_info_data *md1, *md2; g_return_val_if_fail (sort_data != NULL, 0); md1 = g_hash_table_lookup (sort_data->message_infos, uid1); md2 = g_hash_table_lookup (sort_data->message_infos, uid2); g_return_val_if_fail (md1 != NULL, 0); g_return_val_if_fail (md1->mi != NULL, 0); g_return_val_if_fail (md2 != NULL, 0); g_return_val_if_fail (md2->mi != NULL, 0); if (!sort_data->ml || sort_data->folder != sort_data->ml->folder || g_cancellable_is_cancelled (sort_data->cancellable)) return 0; for (i = 0; res == 0 && i < sort_data->sort_columns->len && !g_cancellable_is_cancelled (sort_data->cancellable); i++) { gpointer v1, v2; struct sort_column_data *scol = g_ptr_array_index (sort_data->sort_columns, i); if (md1->values->len <= i) { v1 = ml_tree_value_at_ex (NULL, NULL, scol->col->compare_col, md1->mi, sort_data->ml); g_ptr_array_add (md1->values, v1); } else { v1 = g_ptr_array_index (md1->values, i); } if (md2->values->len <= i) { v2 = ml_tree_value_at_ex (NULL, NULL, scol->col->compare_col, md2->mi, sort_data->ml); g_ptr_array_add (md2->values, v2); } else { v2 = g_ptr_array_index (md2->values, i); } if (v1 != NULL && v2 != NULL) { res = (*scol->col->compare) (v1, v2, sort_data->cmp_cache); } else if (v1 != NULL || v2 != NULL) { res = v1 == NULL ? -1 : 1; } if (!scol->ascending) res = res * (-1); } if (res == 0) res = camel_folder_cmp_uids (sort_data->folder, uid1, uid2); return res; } static void free_message_info_data (gpointer uid, struct sort_message_info_data *data, struct sort_array_data *sort_data) { if (data->values) { /* values in this array are not newly allocated, even ml_tree_value_at_ex * returns gpointer, not a gconstpointer */ g_ptr_array_free (data->values, TRUE); } camel_folder_free_message_info (sort_data->folder, data->mi); g_free (data); } static void ml_sort_uids_by_tree (MessageList *ml, GPtrArray *uids, GCancellable *cancellable) { ETreeTableAdapter *adapter; ETableSortInfo *sort_info; ETableHeader *full_header; struct sort_array_data sort_data; guint i, len; if (g_cancellable_is_cancelled (cancellable)) return; g_return_if_fail (ml != NULL); g_return_if_fail (ml->folder != NULL); g_return_if_fail (uids != NULL); adapter = e_tree_get_table_adapter (E_TREE (ml)); g_return_if_fail (adapter != NULL); sort_info = e_tree_table_adapter_get_sort_info (adapter); full_header = e_tree_table_adapter_get_header (adapter); if (!sort_info || uids->len == 0 || !full_header || e_table_sort_info_sorting_get_count (sort_info) == 0) { camel_folder_sort_uids (ml->folder, uids); return; } len = e_table_sort_info_sorting_get_count (sort_info); sort_data.ml = ml; sort_data.folder = g_object_ref (ml->folder); sort_data.sort_columns = g_ptr_array_sized_new (len); sort_data.message_infos = g_hash_table_new (g_str_hash, g_str_equal); sort_data.cmp_cache = e_table_sorting_utils_create_cmp_cache (); sort_data.cancellable = cancellable; for (i = 0; i < len && ml->folder == sort_data.folder && !g_cancellable_is_cancelled (cancellable); i++) { ETableSortColumn scol; struct sort_column_data *data = g_new0 (struct sort_column_data, 1); scol = e_table_sort_info_sorting_get_nth (sort_info, i); data->ascending = scol.ascending; data->col = e_table_header_get_column_by_col_idx (full_header, scol.column); if (data->col == NULL) data->col = e_table_header_get_column (full_header, e_table_header_count (full_header) - 1); g_ptr_array_add (sort_data.sort_columns, data); } camel_folder_summary_prepare_fetch_all (ml->folder->summary, NULL); for (i = 0; i < uids->len && ml->folder == sort_data.folder && !g_cancellable_is_cancelled (cancellable); i++) { gchar *uid; CamelMessageInfo *mi; struct sort_message_info_data *md; uid = g_ptr_array_index (uids, i); mi = camel_folder_get_message_info (sort_data.folder, uid); if (!mi) { g_warning ("%s: Cannot find uid '%s' in folder '%s'", G_STRFUNC, uid, camel_folder_get_full_name (sort_data.folder)); continue; } md = g_new0 (struct sort_message_info_data, 1); md->mi = mi; md->values = g_ptr_array_sized_new (len); g_hash_table_insert (sort_data.message_infos, uid, md); } if (sort_data.folder == ml->folder && !g_cancellable_is_cancelled (cancellable)) g_qsort_with_data (uids->pdata, uids->len, sizeof (gpointer), cmp_array_uids, &sort_data); camel_folder_summary_unlock (sort_data.folder->summary, CAMEL_FOLDER_SUMMARY_SUMMARY_LOCK); g_hash_table_foreach (sort_data.message_infos, (GHFunc) free_message_info_data, &sort_data); g_hash_table_destroy (sort_data.message_infos); g_ptr_array_foreach (sort_data.sort_columns, (GFunc) g_free, NULL); g_ptr_array_free (sort_data.sort_columns, TRUE); e_table_sorting_utils_free_cmp_cache (sort_data.cmp_cache); g_object_unref (sort_data.folder); } /* ** REGENERATE MESSAGELIST ********************************************** */ struct _regen_list_msg { MailMsg base; gint complete; MessageList *ml; gchar *search; gchar *hideexpr; CamelFolderChangeInfo *changes; gboolean dotree; /* we are building a tree */ gboolean hidedel; /* we want to/dont want to show deleted messages */ gboolean hidejunk; /* we want to/dont want to show junk messages */ gboolean thread_subject; gboolean scroll_to_cursor; /* whether ensure scrolling to the cursor after rebuild */ CamelFolderThread *tree; CamelFolder *folder; GPtrArray *summary; gint last_row; /* last selected (cursor) row */ xmlDoc *expand_state; /* stored expanded state of the previous view */ }; /* * maintain copy of summary * * any new messages added * any removed removed, etc. * * use vfolder to implement searches ??? */ static gchar * regen_list_desc (struct _regen_list_msg *m) { return g_strdup (_("Generating message list")); } static void regen_list_exec (struct _regen_list_msg *m, GCancellable *cancellable, GError **error) { GPtrArray *uids, *searchuids = NULL; CamelMessageInfo *info; ETreePath cursor; ETree *tree; gint i; gchar *expr = NULL; GError *local_error = NULL; if (m->folder != m->ml->folder) return; tree = E_TREE (m->ml); cursor = e_tree_get_cursor (tree); if (cursor) m->last_row = e_tree_table_adapter_row_of_node (e_tree_get_table_adapter (tree), cursor); /* if we have hidedeleted on, use a search to find it out, merge with existing search if set */ if (m->hidedel) { if (m->hidejunk) { if (m->search) { expr = g_alloca (strlen (m->search) + 92); sprintf(expr, "(and (match-all (and (not (system-flag \"deleted\")) (not (system-flag \"junk\"))))\n %s)", m->search); } else expr = (gchar *) "(match-all (and (not (system-flag \"deleted\")) (not (system-flag \"junk\"))))"; } else { if (m->search) { expr = g_alloca (strlen (m->search) + 64); sprintf(expr, "(and (match-all (not (system-flag \"deleted\")))\n %s)", m->search); } else expr = (gchar *) "(match-all (not (system-flag \"deleted\")))"; } } else { if (m->hidejunk) { if (m->search) { expr = g_alloca (strlen (m->search) + 64); sprintf(expr, "(and (match-all (not (system-flag \"junk\")))\n %s)", m->search); } else expr = (gchar *) "(match-all (not (system-flag \"junk\")))"; } else { expr = m->search; } } if (expr == NULL) { uids = camel_folder_get_uids (m->folder); } else { gboolean store_has_vjunk = folder_store_supports_vjunk_folder (m->folder); searchuids = uids = camel_folder_search_by_expression ( m->folder, expr, cancellable, &local_error); /* If m->changes is not NULL, then it means we are called from folder_changed event, * thus we will keep the selected message to be sure it doesn't disappear because * it no longer belong to our search filter. */ if (uids && ((m->changes && m->ml->cursor_uid) || m->ml->ensure_uid)) { const gchar *looking_for = m->ml->cursor_uid; /* ensure_uid has precedence of cursor_uid */ if (m->ml->ensure_uid) looking_for = m->ml->ensure_uid; for (i = 0; i < uids->len; i++) { if (g_str_equal (looking_for, uids->pdata[i])) break; } /* cursor_uid has been filtered out */ if (i == uids->len) { CamelMessageInfo *looking_info = camel_folder_get_message_info (m->folder, looking_for); if (looking_info) { gboolean is_deleted = (camel_message_info_flags (looking_info) & CAMEL_MESSAGE_DELETED) != 0; gboolean is_junk = store_has_vjunk && (camel_message_info_flags (looking_info) & CAMEL_MESSAGE_JUNK) != 0; /* I would really like to check for CAMEL_MESSAGE_FOLDER_FLAGGED on a message, * so I would know whether it was changed locally, and then just check the changes * struct whether change came from the server, but with periodical save it doesn't * matter. So here just check whether the file was deleted and we show it based * on the flag whether we can view deleted messages or not. */ if ((!is_deleted || (is_deleted && !m->hidedel)) && (!is_junk || (is_junk && !m->hidejunk))) g_ptr_array_add (uids, (gpointer) camel_pstring_strdup (looking_for)); camel_folder_free_message_info (m->folder, looking_info); } } } } if (local_error != NULL) { g_propagate_error (error, local_error); return; } /* camel_folder_summary_prepare_fetch_all (m->folder->summary, NULL); */ if (!g_cancellable_is_cancelled (cancellable)) { /* update/build a new tree */ if (m->dotree) { ml_sort_uids_by_tree (m->ml, uids, cancellable); if (m->tree) camel_folder_thread_messages_apply (m->tree, uids); else m->tree = camel_folder_thread_messages_new (m->folder, uids, m->thread_subject); } else { camel_folder_sort_uids (m->ml->folder, uids); m->summary = g_ptr_array_new (); camel_folder_summary_prepare_fetch_all ( m->folder->summary, NULL); for (i = 0; i < uids->len; i++) { info = camel_folder_get_message_info (m->folder, uids->pdata[i]); if (info) g_ptr_array_add (m->summary, info); } } m->complete = TRUE; } if (searchuids) camel_folder_search_free (m->folder, searchuids); else camel_folder_free_uids (m->folder, uids); } static void regen_list_done (struct _regen_list_msg *m) { ETree *tree; GCancellable *cancellable; gboolean searching; cancellable = m->base.cancellable; if (m->ml->priv->destroyed) return; if (!m->complete) return; if (g_cancellable_is_cancelled (cancellable)) return; if (m->ml->folder != m->folder) return; tree = E_TREE (m->ml); if (m->scroll_to_cursor) e_tree_show_cursor_after_reflow (tree); g_signal_handlers_block_by_func (e_tree_get_table_adapter (tree), ml_tree_sorting_changed, m->ml); if (m->ml->search && m->ml->search != m->search) g_free (m->ml->search); m->ml->search = m->search; m->search = NULL; searching = m->ml->search && *m->ml->search; if (m->dotree) { gboolean forcing_expand_state = m->ml->expand_all || m->ml->collapse_all; if (m->ml->just_set_folder) { m->ml->just_set_folder = FALSE; if (m->expand_state) { /* rather load state from disk than use the memory data when changing folders */ xmlFreeDoc (m->expand_state); m->expand_state = NULL; } } if (forcing_expand_state || searching) e_tree_force_expanded_state (tree, (m->ml->expand_all || searching) ? 1 : -1); build_tree (m->ml, m->tree, m->changes); if (m->ml->thread_tree) camel_folder_thread_messages_unref (m->ml->thread_tree); m->ml->thread_tree = m->tree; m->tree = NULL; if (forcing_expand_state || searching) { if (m->ml->folder != NULL && tree != NULL && !searching) save_tree_state (m->ml); /* do not forget to set this back to use the default value... */ e_tree_force_expanded_state (tree, 0); } else load_tree_state (m->ml, m->expand_state); m->ml->expand_all = 0; m->ml->collapse_all = 0; } else build_flat (m->ml, m->summary, m->changes); g_mutex_lock (m->ml->regen_lock); m->ml->regen = g_list_remove (m->ml->regen, m); g_mutex_unlock (m->ml->regen_lock); if (m->ml->regen == NULL && m->ml->pending_select_uid) { gchar *uid; gboolean with_fallback; uid = m->ml->pending_select_uid; m->ml->pending_select_uid = NULL; with_fallback = m->ml->pending_select_fallback; message_list_select_uid (m->ml, uid, with_fallback); g_free (uid); } else if (m->ml->regen == NULL && m->ml->cursor_uid == NULL && m->last_row != -1) { ETreeTableAdapter *etta = e_tree_get_table_adapter (tree); if (m->last_row >= e_table_model_row_count (E_TABLE_MODEL (etta))) m->last_row = e_table_model_row_count (E_TABLE_MODEL (etta)) - 1; if (m->last_row >= 0) { ETreePath path; path = e_tree_table_adapter_node_at_row (etta, m->last_row); if (path) select_path (m->ml, path); } } if (gtk_widget_get_visible (GTK_WIDGET (m->ml))) { if (e_tree_row_count (E_TREE (m->ml)) <= 0) { /* space is used to indicate no search too */ if (m->ml->search && *m->ml->search && strcmp (m->ml->search, " ") != 0) e_tree_set_info_message (tree, _("No message satisfies your search criteria. " "Change search criteria by selecting a new Show message filter from " "the drop down list above or by running a new search either by clearing " "it with Search->Clear menu item or by changing the query above.")); else e_tree_set_info_message (tree, _("There are no messages in this folder.")); } else e_tree_set_info_message (tree, NULL); } g_signal_handlers_unblock_by_func (e_tree_get_table_adapter (tree), ml_tree_sorting_changed, m->ml); g_signal_emit (m->ml, message_list_signals[MESSAGE_LIST_BUILT], 0); m->ml->priv->any_row_changed = FALSE; #if HAVE_CLUTTER if (m->ml->priv->timeline && clutter_timeline_is_playing (m->ml->priv->timeline)) { clutter_timeline_pause (m->ml->priv->timeline); clutter_actor_hide (m->ml->priv->search_texture); } else { ClutterActor *pane = g_object_get_data ((GObject *)m->ml, "actor"); if (pane) { clutter_actor_set_opacity (pane, 0); clutter_actor_animate ( pane, CLUTTER_EASE_OUT_SINE, 150, "opacity", 255, NULL); } } #endif } static void regen_list_free (struct _regen_list_msg *m) { gint i; if (m->summary) { for (i = 0; i < m->summary->len; i++) camel_folder_free_message_info (m->folder, m->summary->pdata[i]); g_ptr_array_free (m->summary, TRUE); } if (m->tree) camel_folder_thread_messages_unref (m->tree); g_free (m->search); g_free (m->hideexpr); g_object_unref (m->folder); if (m->changes) camel_folder_change_info_free (m->changes); /* we have to poke this here as well since we might've been cancelled and regened wont get called */ g_mutex_lock (m->ml->regen_lock); m->ml->regen = g_list_remove (m->ml->regen, m); g_mutex_unlock (m->ml->regen_lock); if (m->expand_state) xmlFreeDoc (m->expand_state); g_object_unref (m->ml); } static MailMsgInfo regen_list_info = { sizeof (struct _regen_list_msg), (MailMsgDescFunc) regen_list_desc, (MailMsgExecFunc) regen_list_exec, (MailMsgDoneFunc) regen_list_done, (MailMsgFreeFunc) regen_list_free }; static gboolean ml_regen_timeout (struct _regen_list_msg *m) { g_mutex_lock (m->ml->regen_lock); m->ml->regen = g_list_prepend (m->ml->regen, m); g_mutex_unlock (m->ml->regen_lock); /* TODO: we should manage our own thread stuff, would make cancelling outstanding stuff easier */ mail_msg_fast_ordered_push (m); m->ml->regen_timeout_msg = NULL; m->ml->regen_timeout_id = 0; return FALSE; } static void mail_regen_cancel (MessageList *ml) { /* cancel any outstanding regeneration requests, not we don't clear, they clear themselves */ if (ml->regen) { GList *link; g_mutex_lock (ml->regen_lock); for (link = ml->regen; link != NULL; link = link->next) { MailMsg *mm = link->data; GCancellable *cancellable; cancellable = mm->cancellable; g_cancellable_cancel (cancellable); } g_mutex_unlock (ml->regen_lock); } /* including unqueued ones */ if (ml->regen_timeout_id) { g_source_remove (ml->regen_timeout_id); ml->regen_timeout_id = 0; mail_msg_unref (ml->regen_timeout_msg); ml->regen_timeout_msg = NULL; } } static void mail_regen_list (MessageList *ml, const gchar *search, const gchar *hideexpr, CamelFolderChangeInfo *changes, gboolean scroll_to_cursor) { struct _regen_list_msg *m; GSettings *settings; gboolean thread_subject; gboolean searching; /* report empty search as NULL, not as one/two-space string */ if (search && (strcmp (search, " ") == 0 || strcmp (search, " ") == 0)) search = NULL; if (ml->folder == NULL) { if (ml->search != search) { g_free (ml->search); ml->search = g_strdup (search); } return; } mail_regen_cancel (ml); settings = g_settings_new ("org.gnome.evolution.mail"); thread_subject = g_settings_get_boolean (settings, "thread-subject"); g_object_unref (settings); #ifndef BROKEN_ETREE /* this can sometimes crash,so ... */ /* see if we need to goto the child thread at all anyway */ /* currently the only case is the flat view with updates and no search */ if (hideexpr == NULL && search == NULL && changes != NULL && !ml->threaded) { build_flat_diff (ml, changes); camel_folder_change_info_free (changes); return; } #endif m = mail_msg_new (®en_list_info); m->ml = g_object_ref (ml); m->search = g_strdup (search); m->hideexpr = g_strdup (hideexpr); m->changes = changes; m->dotree = ml->threaded; m->hidedel = ml->hidedeleted; m->hidejunk = ml->hidejunk; m->thread_subject = thread_subject; m->scroll_to_cursor = scroll_to_cursor; m->folder = g_object_ref (ml->folder); m->last_row = -1; m->expand_state = NULL; if ((!m->hidedel || !m->dotree) && ml->thread_tree) { camel_folder_thread_messages_unref (ml->thread_tree); ml->thread_tree = NULL; } else if (ml->thread_tree) { m->tree = ml->thread_tree; camel_folder_thread_messages_ref (m->tree); } searching = ml->search && *ml->search && !g_str_equal (ml->search, " "); if (e_tree_row_count (E_TREE (ml)) <= 0) { if (gtk_widget_get_visible (GTK_WIDGET (ml))) { /* there is some info why the message list is empty, let it be something useful */ gchar *txt = g_strconcat (_("Generating message list"), "..." , NULL); e_tree_set_info_message (E_TREE (m->ml), txt); g_free (txt); } } else if (ml->priv->any_row_changed && m->dotree && !ml->just_set_folder && !searching) { /* there has been some change on any row, if it was an expand state change, * then let it save; if not, then nothing happen. */ message_list_save_state (ml); } else if (m->dotree && !ml->just_set_folder && !searching) { /* remember actual expand state and restore it after regen */ m->expand_state = e_tree_save_expanded_state_xml (E_TREE (ml)); } /* if we're busy already kick off timeout processing, so normal updates are immediate */ if (ml->regen == NULL) ml_regen_timeout (m); else { ml->regen_timeout_msg = m; ml->regen_timeout_id = g_timeout_add (50, (GSourceFunc) ml_regen_timeout, m); } }