/*
* 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;
/* 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;
}
/* If source widget is packed under 'tree', don't allow any action */
if (is_tree_widget_children (tree, gtk_drag_get_source_widget (context))) {
gdk_drag_status (context, 0, time);
return TRUE;
}
if (EM_IS_FOLDER_TREE (gtk_drag_get_source_widget (context))) {
EMFolderTree *folder_tree;
CamelFolder *folder;
folder_tree = EM_FOLDER_TREE (gtk_drag_get_source_widget (context));
folder = em_folder_tree_get_selected_folder (folder_tree);
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;
}
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;
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);
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;
};
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)
return 0;
for (i = 0; res == 0 && i < sort_data->sort_columns->len; 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)
{
ETreeTableAdapter *adapter;
ETableSortInfo *sort_info;
ETableHeader *full_header;
struct sort_array_data sort_data;
guint i, len;
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 ();
for (i = 0; i < len && ml->folder == sort_data.folder; 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; 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_qsort_with_data (uids->pdata, uids->len, sizeof (gpointer), cmp_array_uids, &sort_data);
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 (!camel_folder_has_search_capability (m->folder)) {
/* if we have no search capability, dont let search or hide deleted work */
expr = NULL;
} else 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);
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. Either clear search with Search->Clear menu item or change it."));
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);
}
}