/* * 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: * Jeffrey Stedfast * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "em-vfolder-editor-rule.h" #include "libemail-engine/e-mail-folder-utils.h" #include "libemail-engine/e-mail-session.h" #include "libemail-engine/mail-mt.h" #include "libemail-engine/mail-ops.h" #include "libemail-engine/mail-tools.h" #include "em-utils.h" #include "em-folder-tree.h" #include "em-folder-utils.h" #include "em-folder-selector.h" #include "em-folder-properties.h" #include "em-event.h" #include "mail-send-recv.h" #include "mail-vfolder-ui.h" #include "e-mail-ui-session.h" #define d(x) #define EM_FOLDER_TREE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), EM_TYPE_FOLDER_TREE, EMFolderTreePrivate)) typedef struct _AsyncContext AsyncContext; #define EM_FOLDER_TREE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), EM_TYPE_FOLDER_TREE, EMFolderTreePrivate)) struct _selected_uri { gchar *key; /* store:path or account/path */ gchar *uri; CamelService *service; gchar *path; }; struct _EMFolderTreePrivate { EMailSession *session; EAlertSink *alert_sink; /* selected_uri structures of each path pending selection. */ GSList *select_uris; /* Removed as they're encountered, so use this * to find URI's not presnet but selected. */ GHashTable *select_uris_table; guint32 excluded; gboolean (*excluded_func) (EMFolderTree *folder_tree, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); gpointer excluded_data; guint cursor_set:1; /* set to TRUE means we or something * else has set the cursor, otherwise * we need to set it when we set the * selection */ guint autoscroll_id; guint autoexpand_id; GtkTreeRowReference *autoexpand_row; guint loading_row_id; guint loaded_row_id; GtkTreeRowReference *drag_row; gboolean skip_double_click; GtkCellRenderer *text_renderer; PangoEllipsizeMode ellipsize; GtkWidget *selectable; /* an ESelectable, where to pass selectable calls */ /* Signal handler IDs */ gulong selection_changed_handler_id; }; struct _AsyncContext { EActivity *activity; EMFolderTree *folder_tree; GtkTreeRowReference *root; gchar *full_name; }; enum { PROP_0, PROP_ALERT_SINK, PROP_COPY_TARGET_LIST, PROP_ELLIPSIZE, PROP_MODEL, PROP_PASTE_TARGET_LIST, PROP_SESSION }; enum { FOLDER_ACTIVATED, /* aka double-clicked or user hit enter */ FOLDER_SELECTED, POPUP_EVENT, HIDDEN_KEY_EVENT, LAST_SIGNAL }; /* Drag & Drop types */ enum DndDragType { DND_DRAG_TYPE_FOLDER, /* drag an evo folder */ DND_DRAG_TYPE_TEXT_URI_LIST, /* drag to an mbox file */ NUM_DRAG_TYPES }; enum DndDropType { DND_DROP_TYPE_UID_LIST, /* drop a list of message uids */ DND_DROP_TYPE_FOLDER, /* drop an evo folder */ DND_DROP_TYPE_MESSAGE_RFC822, /* drop a message/rfc822 stream */ DND_DROP_TYPE_TEXT_URI_LIST, /* drop an mbox file */ NUM_DROP_TYPES }; static GtkTargetEntry drag_types[] = { { (gchar *) "x-folder", 0, DND_DRAG_TYPE_FOLDER }, { (gchar *) "text/uri-list", 0, DND_DRAG_TYPE_TEXT_URI_LIST }, }; static GtkTargetEntry drop_types[] = { { (gchar *) "x-uid-list" , 0, DND_DROP_TYPE_UID_LIST }, { (gchar *) "x-folder", 0, DND_DROP_TYPE_FOLDER }, { (gchar *) "message/rfc822", 0, DND_DROP_TYPE_MESSAGE_RFC822 }, { (gchar *) "text/uri-list", 0, DND_DROP_TYPE_TEXT_URI_LIST }, }; static GdkAtom drag_atoms[NUM_DRAG_TYPES]; static GdkAtom drop_atoms[NUM_DROP_TYPES]; static guint signals[LAST_SIGNAL] = { 0 }; struct _folder_tree_selection_data { GtkTreeModel *model; GtkTreeIter *iter; gboolean set; }; /* Forward Declarations */ static void em_folder_tree_selectable_init (ESelectableInterface *interface); G_DEFINE_TYPE_WITH_CODE ( EMFolderTree, em_folder_tree, GTK_TYPE_TREE_VIEW, G_IMPLEMENT_INTERFACE ( E_TYPE_SELECTABLE, em_folder_tree_selectable_init)) static void async_context_free (AsyncContext *context) { if (context->activity != NULL) g_object_unref (context->activity); if (context->folder_tree != NULL) g_object_unref (context->folder_tree); gtk_tree_row_reference_free (context->root); g_free (context->full_name); g_slice_free (AsyncContext, context); } static void folder_tree_get_folder_info_cb (CamelStore *store, GAsyncResult *result, AsyncContext *context) { struct _EMFolderTreeModelStoreInfo *si; CamelFolderInfo *folder_info; CamelFolderInfo *child_info; EAlertSink *alert_sink; GtkTreeView *tree_view; GtkTreeModel *model; GtkTreePath *path; GtkTreeIter root; GtkTreeIter iter; GtkTreeIter titer; gboolean is_store; gboolean iter_is_placeholder; gboolean valid; GError *error = NULL; alert_sink = e_activity_get_alert_sink (context->activity); folder_info = camel_store_get_folder_info_finish ( store, result, &error); tree_view = GTK_TREE_VIEW (context->folder_tree); model = gtk_tree_view_get_model (tree_view); /* Check if our parent folder has been deleted/unsubscribed. */ if (!gtk_tree_row_reference_valid (context->root)) { g_clear_error (&error); goto exit; } path = gtk_tree_row_reference_get_path (context->root); valid = gtk_tree_model_get_iter (model, &root, path); g_return_if_fail (valid); gtk_tree_model_get (model, &root, COL_BOOL_IS_STORE, &is_store, -1); /* If we had an error, then we need to re-set the * load subdirs state and collapse the node. */ if (error != NULL) { gtk_tree_store_set ( GTK_TREE_STORE (model), &root, COL_BOOL_LOAD_SUBDIRS, TRUE, -1); gtk_tree_view_collapse_row (tree_view, path); } gtk_tree_path_free (path); if (e_activity_handle_cancellation (context->activity, error)) { g_warn_if_fail (folder_info == NULL); async_context_free (context); g_error_free (error); return; /* XXX POP3 stores always return a "no folder" error because they * have no folder hierarchy to scan. Just ignore the error. */ } else if (g_error_matches ( error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER)) { g_warn_if_fail (folder_info == NULL); async_context_free (context); g_error_free (error); return; } else if (error != NULL) { g_warn_if_fail (folder_info == NULL); e_alert_submit ( alert_sink, "mail:folder-open", error->message, NULL); async_context_free (context); g_error_free (error); return; } /* If we've just set up an NNTP account, for example, and haven't * subscribed to any folders yet, folder_info may legitimately be * NULL at this point. We handle that case below. Proceed. */ /* Check if the store has been removed. */ si = em_folder_tree_model_lookup_store_info ( EM_FOLDER_TREE_MODEL (model), store); if (si == NULL) goto exit; /* Make sure we still need to load the tree subfolders. */ iter_is_placeholder = FALSE; /* Get the first child (which will be a placeholder row). */ valid = gtk_tree_model_iter_children (model, &iter, &root); /* Traverse to the last valid iter, or the placeholder row. */ while (valid) { gboolean is_store_node = FALSE; gboolean is_folder_node = FALSE; titer = iter; /* Preserve the last valid iter */ gtk_tree_model_get ( model, &iter, COL_BOOL_IS_STORE, &is_store_node, COL_BOOL_IS_FOLDER, &is_folder_node, -1); /* Stop on a "Loading..." placeholder row. */ if (!is_store_node && !is_folder_node) { iter_is_placeholder = TRUE; break; } valid = gtk_tree_model_iter_next (model, &iter); } iter = titer; child_info = folder_info; /* FIXME Camel's IMAP code is totally on crack here: the * folder_info we got back should be for the folder * we're expanding, and folder_info->child should be * what we want to fill our tree with... *sigh* */ if (folder_info != NULL) { gboolean names_match; names_match = (g_strcmp0 ( folder_info->full_name, context->full_name) == 0); if (names_match) { child_info = folder_info->child; if (child_info == NULL) child_info = folder_info->next; } } /* The folder being expanded has no children after all. Remove * the "Loading..." placeholder row and collapse the parent. */ if (child_info == NULL) { if (iter_is_placeholder) gtk_tree_store_remove (GTK_TREE_STORE (model), &iter); if (is_store) { path = gtk_tree_model_get_path (model, &root); gtk_tree_view_collapse_row (tree_view, path); gtk_tree_path_free (path); goto exit; } } else { while (child_info != NULL) { GtkTreeRowReference *reference; /* Check if we already have this row cached. */ reference = g_hash_table_lookup ( si->full_hash, child_info->full_name); if (reference == NULL) { /* If we're on a placeholder row, reuse * the row for the first child folder. */ if (iter_is_placeholder) iter_is_placeholder = FALSE; else gtk_tree_store_append ( GTK_TREE_STORE (model), &iter, &root); em_folder_tree_model_set_folder_info ( EM_FOLDER_TREE_MODEL (model), &iter, si, child_info, TRUE); } child_info = child_info->next; } /* Remove the "Loading..." placeholder row. */ if (iter_is_placeholder) gtk_tree_store_remove (GTK_TREE_STORE (model), &iter); } gtk_tree_store_set ( GTK_TREE_STORE (model), &root, COL_BOOL_LOAD_SUBDIRS, FALSE, -1); exit: if (folder_info != NULL) camel_store_free_folder_info (store, folder_info); async_context_free (context); } static void folder_tree_emit_popup_event (EMFolderTree *folder_tree, GdkEvent *event) { g_signal_emit (folder_tree, signals[POPUP_EVENT], 0, event); } static void folder_tree_free_select_uri (struct _selected_uri *u) { g_free (u->uri); if (u->service) g_object_unref (u->service); g_free (u->key); g_free (u->path); g_free (u); } static gboolean folder_tree_select_func (GtkTreeSelection *selection, GtkTreeModel *model, GtkTreePath *path, gboolean selected) { EMFolderTreePrivate *priv; GtkTreeView *tree_view; gboolean is_store; guint32 flags; GtkTreeIter iter; tree_view = gtk_tree_selection_get_tree_view (selection); priv = EM_FOLDER_TREE_GET_PRIVATE (tree_view); if (selected) return TRUE; if (priv->excluded == 0 && priv->excluded_func == NULL) return TRUE; if (!gtk_tree_model_get_iter (model, &iter, path)) return TRUE; if (priv->excluded_func != NULL) return priv->excluded_func ( EM_FOLDER_TREE (tree_view), model, &iter, priv->excluded_data); gtk_tree_model_get ( model, &iter, COL_UINT_FLAGS, &flags, COL_BOOL_IS_STORE, &is_store, -1); if (is_store) flags |= CAMEL_FOLDER_NOSELECT; return (flags & priv->excluded) == 0; } /* NOTE: Removes and frees the selected uri structure */ static void folder_tree_select_uri (EMFolderTree *folder_tree, GtkTreePath *path, struct _selected_uri *u) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeView *tree_view; GtkTreeSelection *selection; tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); gtk_tree_selection_select_path (selection, path); if (!priv->cursor_set) { gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); priv->cursor_set = TRUE; } gtk_tree_view_scroll_to_cell (tree_view, path, NULL, TRUE, 0.8f, 0.0f); g_hash_table_remove (priv->select_uris_table, u->key); priv->select_uris = g_slist_remove (priv->select_uris, u); folder_tree_free_select_uri (u); } static void folder_tree_expand_node (const gchar *key, EMFolderTree *folder_tree) { struct _EMFolderTreeModelStoreInfo *si = NULL; GtkTreeRowReference *row; GtkTreeView *tree_view; GtkTreeModel *model; GtkTreePath *path; EMailSession *session; CamelService *service; const gchar *p; gchar *uid; gsize n; struct _selected_uri *u; if (!(p = strchr (key, '/'))) n = strlen (key); else n = (p - key); uid = g_alloca (n + 1); memcpy (uid, key, n); uid[n] = '\0'; tree_view = GTK_TREE_VIEW (folder_tree); model = gtk_tree_view_get_model (tree_view); session = em_folder_tree_get_session (folder_tree); service = camel_session_ref_service (CAMEL_SESSION (session), uid); if (CAMEL_IS_STORE (service)) si = em_folder_tree_model_lookup_store_info ( EM_FOLDER_TREE_MODEL (model), CAMEL_STORE (service)); if (service != NULL) g_object_unref (service); if (si == NULL) return; if (p != NULL && p[1]) { if (!(row = g_hash_table_lookup (si->full_hash, p + 1))) return; } else row = si->row; path = gtk_tree_row_reference_get_path (row); gtk_tree_view_expand_to_path (tree_view, path); u = g_hash_table_lookup (folder_tree->priv->select_uris_table, key); if (u) folder_tree_select_uri (folder_tree, path, u); gtk_tree_path_free (path); } static void folder_tree_maybe_expand_row (EMFolderTreeModel *model, GtkTreePath *tree_path, GtkTreeIter *iter, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; CamelStore *store; gchar *full_name; gchar *key; const gchar *uid; struct _selected_uri *u; gtk_tree_model_get ( GTK_TREE_MODEL (model), iter, COL_STRING_FULL_NAME, &full_name, COL_POINTER_CAMEL_STORE, &store, -1); uid = camel_service_get_uid (CAMEL_SERVICE (store)); key = g_strdup_printf ("%s/%s", uid, full_name ? full_name : ""); u = g_hash_table_lookup (priv->select_uris_table, key); if (u) { gchar *c = strrchr (key, '/'); *c = '\0'; folder_tree_expand_node (key, folder_tree); folder_tree_select_uri (folder_tree, tree_path, u); } g_free (full_name); g_free (key); } static void folder_tree_clear_selected_list (EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; g_slist_foreach (priv->select_uris, (GFunc) folder_tree_free_select_uri, NULL); g_slist_free (priv->select_uris); g_hash_table_destroy (priv->select_uris_table); priv->select_uris = NULL; priv->select_uris_table = g_hash_table_new (g_str_hash, g_str_equal); priv->cursor_set = FALSE; } static void folder_tree_cell_edited_cb (EMFolderTree *folder_tree, const gchar *path_string, const gchar *new_name) { CamelFolderInfo *folder_info; CamelStore *store; GtkTreeView *tree_view; GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; gchar *old_name = NULL; gchar *old_full_name = NULL; gchar *new_full_name = NULL; gchar *folder_uri; gchar **strv; gpointer parent; guint index; GError *local_error = NULL; /* XXX Consider splitting this into separate async functions: * em_folder_tree_rename_folder_async() * em_folder_tree_rename_folder_finish() */ parent = gtk_widget_get_toplevel (GTK_WIDGET (folder_tree)); parent = gtk_widget_is_toplevel (parent) ? parent : NULL; tree_view = GTK_TREE_VIEW (folder_tree); model = gtk_tree_view_get_model (tree_view); path = gtk_tree_path_new_from_string (path_string); gtk_tree_model_get_iter (model, &iter, path); gtk_tree_path_free (path); gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_DISPLAY_NAME, &old_name, COL_STRING_FULL_NAME, &old_full_name, -1); if (!old_name || !old_full_name || g_strcmp0 (new_name, old_name) == 0) goto exit; /* Check for invalid characters. */ if (strchr (new_name, '/') != NULL) { e_alert_run_dialog_for_args ( parent, "mail:no-rename-folder", old_name, new_name, _("Folder names cannot contain '/'"), NULL); goto exit; } /* Build the new name from the old name. */ strv = g_strsplit_set (old_full_name, "/", 0); index = g_strv_length (strv) - 1; g_free (strv[index]); strv[index] = g_strdup (new_name); new_full_name = g_strjoinv ("/", strv); g_strfreev (strv); /* Check for duplicate folder name. */ /* FIXME camel_store_get_folder_info() may block. */ folder_info = camel_store_get_folder_info_sync ( store, new_full_name, CAMEL_STORE_FOLDER_INFO_FAST, NULL, NULL); if (folder_info != NULL) { e_alert_run_dialog_for_args ( parent, "mail:no-rename-folder-exists", old_name, new_name, NULL); camel_store_free_folder_info (store, folder_info); goto exit; } /* FIXME camel_store_rename_folder_sync() may block. */ camel_store_rename_folder_sync ( store, old_full_name, new_full_name, NULL, &local_error); if (local_error != NULL) { e_alert_run_dialog_for_args ( parent, "mail:no-rename-folder", old_full_name, new_full_name, local_error->message, NULL); g_error_free (local_error); goto exit; } folder_uri = e_mail_folder_uri_build (store, new_full_name); em_folder_tree_set_selected (folder_tree, folder_uri, FALSE); g_free (folder_uri); exit: g_free (old_name); g_free (old_full_name); g_free (new_full_name); } static gboolean subdirs_contain_unread (GtkTreeModel *model, GtkTreeIter *root) { guint unread; GtkTreeIter iter; if (!gtk_tree_model_iter_children (model, &iter, root)) return FALSE; do { gtk_tree_model_get (model, &iter, COL_UINT_UNREAD, &unread, -1); if (unread) return TRUE; if (gtk_tree_model_iter_has_child (model, &iter)) if (subdirs_contain_unread (model, &iter)) return TRUE; } while (gtk_tree_model_iter_next (model, &iter)); return FALSE; } static void folder_tree_render_display_name (GtkTreeViewColumn *column, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter) { CamelService *service; PangoWeight weight; gboolean is_store, bold, subdirs_unread = FALSE; gboolean editable; guint unread; gchar *name; gtk_tree_model_get ( model, iter, COL_STRING_DISPLAY_NAME, &name, COL_POINTER_CAMEL_STORE, &service, COL_BOOL_IS_STORE, &is_store, COL_UINT_UNREAD, &unread, -1); g_object_get (renderer, "editable", &editable, NULL); bold = is_store || unread; if (gtk_tree_model_iter_has_child (model, iter)) { gboolean expanded = TRUE; g_object_get (renderer, "is-expanded", &expanded, NULL); if (!bold || !expanded) subdirs_unread = subdirs_contain_unread (model, iter); } bold = !editable && (bold || subdirs_unread); weight = bold ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL; g_object_set (renderer, "weight", weight, NULL); if (is_store) { const gchar *display_name; display_name = camel_service_get_display_name (service); g_object_set (renderer, "text", display_name, NULL); } else if (!editable && unread > 0) { gchar *name_and_unread; /* Translators: This is the string used for displaying the * folder names in folder trees. The first "%s" will be * replaced by the folder's name and "%u" will be replaced * with the number of unread messages in the folder. The * second %s will be replaced with a "+" letter for collapsed * folders with unread messages in some subfolder too, * or with an empty string for other cases. * * Most languages should translate this as "%s (%u%s)". The * languages that use localized digits (like Persian) may * need to replace "%u" with "%Iu". Right-to-left languages * (like Arabic and Hebrew) may need to add bidirectional * formatting codes to take care of the cases the folder * name appears in either direction. * * Do not translate the "folder-display|" part. Remove it * from your translation. */ name_and_unread = g_strdup_printf ( C_("folder-display", "%s (%u%s)"), name, unread, subdirs_unread ? "+" : ""); g_object_set (renderer, "text", name_and_unread, NULL); g_free (name_and_unread); } else { g_object_set (renderer, "text", name, NULL); } g_free (name); } static void folder_tree_render_icon (GtkTreeViewColumn *column, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter) { GtkTreeSelection *selection; GtkTreePath *drag_dest_row; GtkWidget *tree_view; GIcon *icon; guint unread; guint old_unread; gchar *icon_name; gboolean is_selected; gboolean is_drafts = FALSE; gboolean is_drag_dest = FALSE; guint32 fi_flags = 0; gtk_tree_model_get ( model, iter, COL_STRING_ICON_NAME, &icon_name, COL_UINT_UNREAD_LAST_SEL, &old_unread, COL_UINT_UNREAD, &unread, COL_BOOL_IS_DRAFT, &is_drafts, COL_UINT_FLAGS, &fi_flags, -1); if (icon_name == NULL) return; tree_view = gtk_tree_view_column_get_tree_view (column); selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)); is_selected = gtk_tree_selection_iter_is_selected (selection, iter); gtk_tree_view_get_drag_dest_row ( GTK_TREE_VIEW (tree_view), &drag_dest_row, NULL); if (drag_dest_row != NULL) { GtkTreePath *path; path = gtk_tree_model_get_path (model, iter); if (gtk_tree_path_compare (path, drag_dest_row) == 0) is_drag_dest = TRUE; gtk_tree_path_free (path); gtk_tree_path_free (drag_dest_row); } if (g_strcmp0 (icon_name, "folder") == 0) { if (is_selected) { g_free (icon_name); icon_name = g_strdup ("folder-open"); } else if (is_drag_dest) { g_free (icon_name); icon_name = g_strdup ("folder-drag-accept"); } } icon = g_themed_icon_new (icon_name); /* Show an emblem if there's new mail. */ if (!is_selected && unread > old_unread && !is_drafts && !(fi_flags & CAMEL_FOLDER_VIRTUAL)) { GIcon *temp_icon; GEmblem *emblem; temp_icon = g_themed_icon_new ("emblem-new"); emblem = g_emblem_new (temp_icon); g_object_unref (temp_icon); temp_icon = g_emblemed_icon_new (icon, emblem); g_object_unref (emblem); g_object_unref (icon); icon = temp_icon; } g_object_set (renderer, "gicon", icon, NULL); g_object_unref (icon); g_free (icon_name); } static void folder_tree_selection_changed_cb (EMFolderTree *folder_tree, GtkTreeSelection *selection) { GtkTreeModel *model; GtkTreeIter iter; GList *list; CamelStore *store = NULL; CamelFolderInfoFlags flags = 0; guint unread = 0; guint old_unread = 0; gchar *folder_name = NULL; list = gtk_tree_selection_get_selected_rows (selection, &model); if (list == NULL) goto exit; gtk_tree_model_get_iter (model, &iter, list->data); gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_FULL_NAME, &folder_name, COL_UINT_FLAGS, &flags, COL_UINT_UNREAD, &unread, COL_UINT_UNREAD_LAST_SEL, &old_unread, -1); /* Sync unread counts to distinguish new incoming mail. */ if (unread != old_unread) gtk_tree_store_set ( GTK_TREE_STORE (model), &iter, COL_UINT_UNREAD_LAST_SEL, unread, -1); exit: g_signal_emit ( folder_tree, signals[FOLDER_SELECTED], 0, store, folder_name, flags); g_free (folder_name); g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL); g_list_free (list); } static void folder_tree_copy_expanded_cb (GtkTreeView *unused, GtkTreePath *path, GtkTreeView *tree_view) { gtk_tree_view_expand_row (tree_view, path, FALSE); } static void folder_tree_copy_selection_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, GtkTreeView *tree_view) { GtkTreeSelection *selection; selection = gtk_tree_view_get_selection (tree_view); gtk_tree_selection_select_path (selection, path); /* Center the tree view on the selected path. */ gtk_tree_view_scroll_to_cell (tree_view, path, NULL, TRUE, 0.5, 0.0); } static void folder_tree_copy_state (EMFolderTree *folder_tree) { GtkTreeSelection *selection; GtkTreeView *tree_view; GtkTreeModel *model; tree_view = GTK_TREE_VIEW (folder_tree); model = gtk_tree_view_get_model (tree_view); selection = em_folder_tree_model_get_selection ( EM_FOLDER_TREE_MODEL (model)); if (selection == NULL) return; gtk_tree_view_map_expanded_rows ( tree_view, (GtkTreeViewMappingFunc) folder_tree_copy_expanded_cb, folder_tree); gtk_tree_selection_selected_foreach ( selection, (GtkTreeSelectionForeachFunc) folder_tree_copy_selection_cb, folder_tree); } static void folder_tree_set_alert_sink (EMFolderTree *folder_tree, EAlertSink *alert_sink) { g_return_if_fail (E_IS_ALERT_SINK (alert_sink)); g_return_if_fail (folder_tree->priv->alert_sink == NULL); folder_tree->priv->alert_sink = g_object_ref (alert_sink); } static void folder_tree_set_session (EMFolderTree *folder_tree, EMailSession *session) { g_return_if_fail (E_IS_MAIL_SESSION (session)); g_return_if_fail (folder_tree->priv->session == NULL); folder_tree->priv->session = g_object_ref (session); } static GtkTargetList * folder_tree_get_copy_target_list (EMFolderTree *folder_tree) { GtkTargetList *target_list = NULL; if (E_IS_SELECTABLE (folder_tree->priv->selectable)) { ESelectable *selectable; selectable = E_SELECTABLE (folder_tree->priv->selectable); target_list = e_selectable_get_copy_target_list (selectable); } return target_list; } static GtkTargetList * folder_tree_get_paste_target_list (EMFolderTree *folder_tree) { GtkTargetList *target_list = NULL; if (E_IS_SELECTABLE (folder_tree->priv->selectable)) { ESelectable *selectable; selectable = E_SELECTABLE (folder_tree->priv->selectable); target_list = e_selectable_get_paste_target_list (selectable); } return target_list; } static void folder_tree_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_ALERT_SINK: folder_tree_set_alert_sink ( EM_FOLDER_TREE (object), g_value_get_object (value)); return; case PROP_ELLIPSIZE: em_folder_tree_set_ellipsize ( EM_FOLDER_TREE (object), g_value_get_enum (value)); return; case PROP_MODEL: gtk_tree_view_set_model ( GTK_TREE_VIEW (object), g_value_get_object (value)); return; case PROP_SESSION: folder_tree_set_session ( EM_FOLDER_TREE (object), g_value_get_object (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void folder_tree_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_ALERT_SINK: g_value_set_object ( value, em_folder_tree_get_alert_sink ( EM_FOLDER_TREE (object))); return; case PROP_COPY_TARGET_LIST: g_value_set_boxed ( value, folder_tree_get_copy_target_list ( EM_FOLDER_TREE (object))); return; case PROP_ELLIPSIZE: g_value_set_enum ( value, em_folder_tree_get_ellipsize ( EM_FOLDER_TREE (object))); return; case PROP_MODEL: g_value_set_object ( value, gtk_tree_view_get_model ( GTK_TREE_VIEW (object))); return; case PROP_PASTE_TARGET_LIST: g_value_set_boxed ( value, folder_tree_get_paste_target_list ( EM_FOLDER_TREE (object))); return; case PROP_SESSION: g_value_set_object ( value, em_folder_tree_get_session ( EM_FOLDER_TREE (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void folder_tree_dispose (GObject *object) { EMFolderTreePrivate *priv; GtkTreeModel *model; priv = EM_FOLDER_TREE_GET_PRIVATE (object); model = gtk_tree_view_get_model (GTK_TREE_VIEW (object)); if (priv->loaded_row_id != 0) { g_signal_handler_disconnect (model, priv->loaded_row_id); priv->loaded_row_id = 0; } if (priv->autoscroll_id != 0) { g_source_remove (priv->autoscroll_id); priv->autoscroll_id = 0; } if (priv->autoexpand_id != 0) { gtk_tree_row_reference_free (priv->autoexpand_row); priv->autoexpand_row = NULL; g_source_remove (priv->autoexpand_id); priv->autoexpand_id = 0; } if (priv->alert_sink != NULL) { g_object_unref (priv->alert_sink); priv->alert_sink = NULL; } if (priv->session != NULL) { g_object_unref (priv->session); priv->session = NULL; } if (priv->text_renderer != NULL) { g_object_unref (priv->text_renderer); priv->text_renderer = NULL; } /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (em_folder_tree_parent_class)->dispose (object); } static void folder_tree_finalize (GObject *object) { EMFolderTreePrivate *priv; priv = EM_FOLDER_TREE_GET_PRIVATE (object); if (priv->select_uris != NULL) { g_slist_foreach ( priv->select_uris, (GFunc) folder_tree_free_select_uri, NULL); g_slist_free (priv->select_uris); priv->select_uris = NULL; } if (priv->select_uris_table) { g_hash_table_destroy (priv->select_uris_table); priv->select_uris_table = NULL; } /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (em_folder_tree_parent_class)->finalize (object); } static void folder_tree_constructed (GObject *object) { EMFolderTreePrivate *priv; GtkTreeSelection *selection; GtkTreeViewColumn *column; GtkCellRenderer *renderer; GtkTreeView *tree_view; GtkTreeModel *model; gulong handler_id; priv = EM_FOLDER_TREE_GET_PRIVATE (object); /* Chain up to parent's constructed() method. */ G_OBJECT_CLASS (em_folder_tree_parent_class)->constructed (object); tree_view = GTK_TREE_VIEW (object); model = gtk_tree_view_get_model (tree_view); selection = gtk_tree_view_get_selection (tree_view); handler_id = g_signal_connect ( model, "loading-row", G_CALLBACK (folder_tree_maybe_expand_row), object); priv->loading_row_id = handler_id; handler_id = g_signal_connect ( model, "loaded-row", G_CALLBACK (folder_tree_maybe_expand_row), object); priv->loaded_row_id = handler_id; handler_id = g_signal_connect_swapped ( selection, "changed", G_CALLBACK (folder_tree_selection_changed_cb), object); priv->selection_changed_handler_id = handler_id; column = gtk_tree_view_column_new (); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_pixbuf_new (); gtk_tree_view_column_pack_start (column, renderer, FALSE); gtk_tree_view_column_add_attribute ( column, renderer, "visible", COL_BOOL_IS_FOLDER); gtk_tree_view_column_set_cell_data_func ( column, renderer, (GtkTreeCellDataFunc) folder_tree_render_icon, NULL, NULL); renderer = gtk_cell_renderer_text_new (); gtk_tree_view_column_pack_start (column, renderer, TRUE); gtk_tree_view_column_set_cell_data_func ( column, renderer, (GtkTreeCellDataFunc) folder_tree_render_display_name, NULL, NULL); priv->text_renderer = g_object_ref (renderer); g_object_bind_property ( object, "ellipsize", renderer, "ellipsize", G_BINDING_SYNC_CREATE); g_signal_connect_swapped ( renderer, "edited", G_CALLBACK (folder_tree_cell_edited_cb), object); gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); gtk_tree_selection_set_select_function ( selection, (GtkTreeSelectionFunc) folder_tree_select_func, NULL, NULL); gtk_tree_view_set_headers_visible (tree_view, FALSE); gtk_tree_view_set_search_column (tree_view, COL_STRING_DISPLAY_NAME); folder_tree_copy_state (EM_FOLDER_TREE (object)); gtk_widget_show (GTK_WIDGET (object)); } static gboolean folder_tree_button_press_event (GtkWidget *widget, GdkEventButton *event) { EMFolderTreePrivate *priv; GtkWidgetClass *widget_class; GtkTreeSelection *selection; GtkTreeView *tree_view; GtkTreePath *path; gulong handler_id; priv = EM_FOLDER_TREE_GET_PRIVATE (widget); tree_view = GTK_TREE_VIEW (widget); selection = gtk_tree_view_get_selection (tree_view); if (gtk_tree_selection_get_mode (selection) == GTK_SELECTION_SINGLE) folder_tree_clear_selected_list (EM_FOLDER_TREE (widget)); priv->cursor_set = TRUE; if (event->button != 3) goto chainup; if (!gtk_tree_view_get_path_at_pos ( tree_view, event->x, event->y, &path, NULL, NULL, NULL)) goto chainup; /* Select and focus the row that was right-clicked, but prevent * a "folder-selected" signal emission since this does not count * as a folder selection in the sense we mean. */ handler_id = priv->selection_changed_handler_id; g_signal_handler_block (selection, handler_id); gtk_tree_selection_select_path (selection, path); gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); g_signal_handler_unblock (selection, handler_id); gtk_tree_path_free (path); folder_tree_emit_popup_event ( EM_FOLDER_TREE (tree_view), (GdkEvent *) event); chainup: /* Chain up to parent's button_press_event() method. */ widget_class = GTK_WIDGET_CLASS (em_folder_tree_parent_class); return widget_class->button_press_event (widget, event); } static gboolean folder_tree_key_press_event (GtkWidget *widget, GdkEventKey *event) { EMFolderTreePrivate *priv; GtkWidgetClass *widget_class; GtkTreeSelection *selection; GtkTreeView *tree_view; if (event && event->type == GDK_KEY_PRESS && (event->keyval == GDK_KEY_space || event->keyval == '.' || event->keyval == ',' || event->keyval == '[' || event->keyval == ']')) { g_signal_emit (widget, signals[HIDDEN_KEY_EVENT], 0, event); return TRUE; } priv = EM_FOLDER_TREE_GET_PRIVATE (widget); tree_view = GTK_TREE_VIEW (widget); selection = gtk_tree_view_get_selection (tree_view); if (gtk_tree_selection_get_mode (selection) == GTK_SELECTION_SINGLE) folder_tree_clear_selected_list (EM_FOLDER_TREE (widget)); priv->cursor_set = TRUE; /* Chain up to parent's key_press_event() method. */ widget_class = GTK_WIDGET_CLASS (em_folder_tree_parent_class); return widget_class->key_press_event (widget, event); } static gboolean folder_tree_popup_menu (GtkWidget *widget) { folder_tree_emit_popup_event (EM_FOLDER_TREE (widget), NULL); return TRUE; } static void folder_tree_row_activated (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column) { EMFolderTreePrivate *priv; GtkTreeModel *model; gchar *folder_name; GtkTreeIter iter; CamelStore *store; CamelFolderInfoFlags flags; priv = EM_FOLDER_TREE_GET_PRIVATE (tree_view); model = gtk_tree_view_get_model (tree_view); if (priv->skip_double_click) return; if (!gtk_tree_model_get_iter (model, &iter, path)) return; gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_FULL_NAME, &folder_name, COL_UINT_FLAGS, &flags, -1); folder_tree_clear_selected_list (EM_FOLDER_TREE (tree_view)); g_signal_emit ( tree_view, signals[FOLDER_SELECTED], 0, store, folder_name, flags); g_signal_emit ( tree_view, signals[FOLDER_ACTIVATED], 0, store, folder_name); g_free (folder_name); } static gboolean folder_tree_test_collapse_row (GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path) { GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter cursor; selection = gtk_tree_view_get_selection (tree_view); if (!gtk_tree_selection_get_selected (selection, &model, &cursor)) goto exit; /* Select the collapsed node IFF it is a * parent of the currently selected folder. */ if (gtk_tree_store_is_ancestor (GTK_TREE_STORE (model), iter, &cursor)) gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); exit: return FALSE; } static void folder_tree_row_expanded (GtkTreeView *tree_view, GtkTreeIter *iter, GtkTreePath *path) { EActivity *activity; GCancellable *cancellable; EMFolderTree *folder_tree; AsyncContext *context; GtkTreeModel *model; CamelStore *store; gchar *full_name; gboolean load; folder_tree = EM_FOLDER_TREE (tree_view); model = gtk_tree_view_get_model (tree_view); gtk_tree_model_get ( model, iter, COL_STRING_FULL_NAME, &full_name, COL_POINTER_CAMEL_STORE, &store, COL_BOOL_LOAD_SUBDIRS, &load, -1); if (!load) { g_free (full_name); return; } gtk_tree_store_set ( GTK_TREE_STORE (model), iter, COL_BOOL_LOAD_SUBDIRS, FALSE, -1); /* Retrieve folder info asynchronously. */ activity = em_folder_tree_new_activity (folder_tree); cancellable = e_activity_get_cancellable (activity); context = g_slice_new0 (AsyncContext); context->activity = activity; context->folder_tree = g_object_ref (folder_tree); context->root = gtk_tree_row_reference_new (model, path); context->full_name = g_strdup (full_name); camel_store_get_folder_info ( store, full_name, CAMEL_STORE_FOLDER_INFO_FAST | CAMEL_STORE_FOLDER_INFO_RECURSIVE | CAMEL_STORE_FOLDER_INFO_SUBSCRIBED, G_PRIORITY_DEFAULT, cancellable, (GAsyncReadyCallback) folder_tree_get_folder_info_cb, context); g_free (full_name); } static void em_folder_tree_class_init (EMFolderTreeClass *class) { GObjectClass *object_class; GtkWidgetClass *widget_class; GtkTreeViewClass *tree_view_class; g_type_class_add_private (class, sizeof (EMFolderTreePrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = folder_tree_set_property; object_class->get_property = folder_tree_get_property; object_class->dispose = folder_tree_dispose; object_class->finalize = folder_tree_finalize; object_class->constructed = folder_tree_constructed; widget_class = GTK_WIDGET_CLASS (class); widget_class->button_press_event = folder_tree_button_press_event; widget_class->key_press_event = folder_tree_key_press_event; widget_class->popup_menu = folder_tree_popup_menu; tree_view_class = GTK_TREE_VIEW_CLASS (class); tree_view_class->row_activated = folder_tree_row_activated; tree_view_class->test_collapse_row = folder_tree_test_collapse_row; tree_view_class->row_expanded = folder_tree_row_expanded; g_object_class_install_property ( object_class, PROP_ALERT_SINK, g_param_spec_object ( "alert-sink", NULL, NULL, E_TYPE_ALERT_SINK, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /* Inherited from ESelectableInterface */ g_object_class_override_property ( object_class, PROP_COPY_TARGET_LIST, "copy-target-list"); g_object_class_install_property ( object_class, PROP_ELLIPSIZE, g_param_spec_enum ( "ellipsize", NULL, NULL, PANGO_TYPE_ELLIPSIZE_MODE, PANGO_ELLIPSIZE_NONE, G_PARAM_READWRITE)); /* XXX We override the GtkTreeView:model property to add * G_PARAM_CONSTRUCT_ONLY so the model is set by the * time we get to folder_tree_constructed(). */ g_object_class_install_property ( object_class, PROP_MODEL, g_param_spec_object ( "model", "TreeView Model", "The model for the tree view", GTK_TYPE_TREE_MODEL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); /* 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", NULL, NULL, E_TYPE_MAIL_SESSION, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); signals[FOLDER_SELECTED] = g_signal_new ( "folder-selected", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EMFolderTreeClass, folder_selected), NULL, NULL, e_marshal_VOID__OBJECT_STRING_UINT, G_TYPE_NONE, 3, CAMEL_TYPE_STORE, G_TYPE_STRING, G_TYPE_UINT); signals[FOLDER_ACTIVATED] = g_signal_new ( "folder-activated", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EMFolderTreeClass, folder_activated), NULL, NULL, e_marshal_VOID__OBJECT_STRING, G_TYPE_NONE, 2, CAMEL_TYPE_STORE, G_TYPE_STRING); signals[POPUP_EVENT] = g_signal_new ( "popup-event", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (EMFolderTreeClass, popup_event), NULL, NULL, g_cclosure_marshal_VOID__BOXED, G_TYPE_NONE, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); signals[HIDDEN_KEY_EVENT] = g_signal_new ( "hidden-key-event", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EMFolderTreeClass, hidden_key_event), NULL, NULL, g_cclosure_marshal_VOID__BOXED, G_TYPE_NONE, 1, GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE); } static void em_folder_tree_init (EMFolderTree *folder_tree) { GHashTable *select_uris_table; AtkObject *a11y; select_uris_table = g_hash_table_new (g_str_hash, g_str_equal); folder_tree->priv = EM_FOLDER_TREE_GET_PRIVATE (folder_tree); folder_tree->priv->select_uris_table = select_uris_table; /* FIXME Gross hack. */ gtk_widget_set_can_focus (GTK_WIDGET (folder_tree), TRUE); a11y = gtk_widget_get_accessible (GTK_WIDGET (folder_tree)); atk_object_set_name (a11y, _("Mail Folder Tree")); } /* Sets a selectable widget, which will be used for update-actions and * select-all selectable interface functions. This can be NULL, then nothing * can be selected and calling selectable function does nothing. */ void em_folder_tree_set_selectable_widget (EMFolderTree *folder_tree, GtkWidget *selectable) { g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); if (selectable != NULL) g_return_if_fail (E_IS_SELECTABLE (selectable)); folder_tree->priv->selectable = selectable; } static void folder_tree_selectable_update_actions (ESelectable *selectable, EFocusTracker *focus_tracker, GdkAtom *clipboard_targets, gint n_clipboard_targets) { EMFolderTree *folder_tree; folder_tree = EM_FOLDER_TREE (selectable); g_return_if_fail (folder_tree != NULL); if (folder_tree->priv->selectable != NULL) { ESelectableInterface *interface; ESelectable *selectable; selectable = E_SELECTABLE (folder_tree->priv->selectable); interface = E_SELECTABLE_GET_INTERFACE (selectable); g_return_if_fail (interface->update_actions != NULL); interface->update_actions ( selectable, focus_tracker, clipboard_targets, n_clipboard_targets); } } static void folder_tree_selectable_cut_clipboard (ESelectable *selectable) { ESelectableInterface *interface; EMFolderTree *folder_tree; GtkWidget *proxy; folder_tree = EM_FOLDER_TREE (selectable); proxy = folder_tree->priv->selectable; if (!E_IS_SELECTABLE (proxy)) return; interface = E_SELECTABLE_GET_INTERFACE (proxy); if (interface->cut_clipboard == NULL) return; if (gtk_widget_get_can_focus (proxy)) gtk_widget_grab_focus (proxy); interface->cut_clipboard (E_SELECTABLE (proxy)); } static void folder_tree_selectable_copy_clipboard (ESelectable *selectable) { ESelectableInterface *interface; EMFolderTree *folder_tree; GtkWidget *proxy; folder_tree = EM_FOLDER_TREE (selectable); proxy = folder_tree->priv->selectable; if (!E_IS_SELECTABLE (proxy)) return; interface = E_SELECTABLE_GET_INTERFACE (proxy); if (interface->copy_clipboard == NULL) return; if (gtk_widget_get_can_focus (proxy)) gtk_widget_grab_focus (proxy); interface->copy_clipboard (E_SELECTABLE (proxy)); } static void folder_tree_selectable_paste_clipboard (ESelectable *selectable) { ESelectableInterface *interface; EMFolderTree *folder_tree; GtkWidget *proxy; folder_tree = EM_FOLDER_TREE (selectable); proxy = folder_tree->priv->selectable; if (!E_IS_SELECTABLE (proxy)) return; interface = E_SELECTABLE_GET_INTERFACE (proxy); if (interface->paste_clipboard == NULL) return; if (gtk_widget_get_can_focus (proxy)) gtk_widget_grab_focus (proxy); interface->paste_clipboard (E_SELECTABLE (proxy)); } static void folder_tree_selectable_delete_selection (ESelectable *selectable) { ESelectableInterface *interface; EMFolderTree *folder_tree; GtkWidget *proxy; folder_tree = EM_FOLDER_TREE (selectable); proxy = folder_tree->priv->selectable; if (!E_IS_SELECTABLE (proxy)) return; interface = E_SELECTABLE_GET_INTERFACE (proxy); if (interface->delete_selection == NULL) return; if (gtk_widget_get_can_focus (proxy)) gtk_widget_grab_focus (proxy); interface->delete_selection (E_SELECTABLE (proxy)); } static void folder_tree_selectable_select_all (ESelectable *selectable) { ESelectableInterface *interface; EMFolderTree *folder_tree; GtkWidget *proxy; folder_tree = EM_FOLDER_TREE (selectable); proxy = folder_tree->priv->selectable; if (!E_IS_SELECTABLE (proxy)) return; interface = E_SELECTABLE_GET_INTERFACE (proxy); if (interface->select_all == NULL) return; if (gtk_widget_get_can_focus (proxy)) gtk_widget_grab_focus (proxy); interface->select_all (E_SELECTABLE (proxy)); } static void em_folder_tree_selectable_init (ESelectableInterface *interface) { interface->update_actions = folder_tree_selectable_update_actions; interface->cut_clipboard = folder_tree_selectable_cut_clipboard; interface->copy_clipboard = folder_tree_selectable_copy_clipboard; interface->paste_clipboard = folder_tree_selectable_paste_clipboard; interface->delete_selection = folder_tree_selectable_delete_selection; interface->select_all = folder_tree_selectable_select_all; } GtkWidget * em_folder_tree_new (EMailSession *session, EAlertSink *alert_sink) { EMFolderTreeModel *model; g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL); g_return_val_if_fail (E_IS_ALERT_SINK (alert_sink), NULL); model = em_folder_tree_model_get_default (); return em_folder_tree_new_with_model (session, alert_sink, model); } GtkWidget * em_folder_tree_new_with_model (EMailSession *session, EAlertSink *alert_sink, EMFolderTreeModel *model) { g_return_val_if_fail (E_IS_MAIL_SESSION (session), NULL); g_return_val_if_fail (E_IS_ALERT_SINK (alert_sink), NULL); g_return_val_if_fail (EM_IS_FOLDER_TREE_MODEL (model), NULL); return g_object_new ( EM_TYPE_FOLDER_TREE, "alert-sink", alert_sink, "session", session, "model", model, NULL); } EActivity * em_folder_tree_new_activity (EMFolderTree *folder_tree) { EActivity *activity; EMailSession *session; EAlertSink *alert_sink; GCancellable *cancellable; g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), NULL); activity = e_activity_new (); alert_sink = em_folder_tree_get_alert_sink (folder_tree); e_activity_set_alert_sink (activity, alert_sink); cancellable = camel_operation_new (); e_activity_set_cancellable (activity, cancellable); g_object_unref (cancellable); session = em_folder_tree_get_session (folder_tree); e_mail_ui_session_add_activity ( E_MAIL_UI_SESSION (session), activity); return activity; } PangoEllipsizeMode em_folder_tree_get_ellipsize (EMFolderTree *folder_tree) { g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), 0); return folder_tree->priv->ellipsize; } void em_folder_tree_set_ellipsize (EMFolderTree *folder_tree, PangoEllipsizeMode ellipsize) { g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); if (ellipsize == folder_tree->priv->ellipsize) return; folder_tree->priv->ellipsize = ellipsize; g_object_notify (G_OBJECT (folder_tree), "ellipsize"); } EAlertSink * em_folder_tree_get_alert_sink (EMFolderTree *folder_tree) { g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), NULL); return folder_tree->priv->alert_sink; } EMailSession * em_folder_tree_get_session (EMFolderTree *folder_tree) { g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), NULL); return folder_tree->priv->session; } static void tree_drag_begin (GtkWidget *widget, GdkDragContext *context, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeSelection *selection; GtkTreeView *tree_view; cairo_surface_t *s; GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; tree_view = GTK_TREE_VIEW (widget); selection = gtk_tree_view_get_selection (tree_view); if (!gtk_tree_selection_get_selected (selection, &model, &iter)) return; path = gtk_tree_model_get_path (model, &iter); priv->drag_row = gtk_tree_row_reference_new (model, path); s = gtk_tree_view_create_row_drag_icon (tree_view, path); gtk_drag_set_icon_surface (context, s); gtk_tree_path_free (path); } static void tree_drag_data_get (GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection, guint info, guint time, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeModel *model; GtkTreePath *src_path; CamelFolder *folder; CamelStore *store; GtkTreeIter iter; gchar *folder_name = NULL; gchar *folder_uri; if (!priv->drag_row || !(src_path = gtk_tree_row_reference_get_path (priv->drag_row))) return; model = gtk_tree_view_get_model (GTK_TREE_VIEW (folder_tree)); if (!gtk_tree_model_get_iter (model, &iter, src_path)) goto fail; gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_FULL_NAME, &folder_name, -1); /* make sure user isn't trying to drag on a placeholder row */ if (folder_name == NULL) goto fail; folder_uri = e_mail_folder_uri_build (store, folder_name); switch (info) { case DND_DRAG_TYPE_FOLDER: /* dragging to a new location in the folder tree */ gtk_selection_data_set ( selection, drag_atoms[info], 8, (guchar *) folder_uri, strlen (folder_uri) + 1); break; case DND_DRAG_TYPE_TEXT_URI_LIST: /* dragging to nautilus or something, probably */ /* FIXME camel_store_get_folder_sync() may block. */ if ((folder = camel_store_get_folder_sync ( store, folder_name, 0, NULL, NULL))) { GPtrArray *uids = camel_folder_get_uids (folder); em_utils_selection_set_urilist (selection, folder, uids); camel_folder_free_uids (folder, uids); g_object_unref (folder); } break; default: abort (); } g_free (folder_uri); fail: gtk_tree_path_free (src_path); g_free (folder_name); } static gboolean ask_drop_folder (EMFolderTree *folder_tree, const gchar *src_folder_uri, const gchar *des_full_name, CamelStore *des_store, gboolean is_move) { const gchar *key = is_move ? "prompt-on-folder-drop-move" : "prompt-on-folder-drop-copy"; EMailSession *session; GSettings *settings; gchar *set_value, *src_folder_name = NULL; GError *error = NULL; GtkWidget *widget; GtkWindow *parent; gint response; g_return_val_if_fail (folder_tree != NULL, FALSE); g_return_val_if_fail (src_folder_uri != NULL, FALSE); g_return_val_if_fail (des_full_name != NULL || des_store != NULL, FALSE); settings = g_settings_new ("org.gnome.evolution.mail"); set_value = g_settings_get_string (settings, key); if (g_strcmp0 (set_value, "never") == 0) { g_object_unref (settings); g_free (set_value); return FALSE; } else if (g_strcmp0 (set_value, "always") == 0) { g_object_unref (settings); g_free (set_value); return TRUE; } g_free (set_value); session = em_folder_tree_get_session (folder_tree); e_mail_folder_uri_parse ( CAMEL_SESSION (session), src_folder_uri, NULL, &src_folder_name, &error); if (error != NULL) { g_warning ( "%s: Failed to convert '%s' to folder name: %s", G_STRFUNC, src_folder_uri, error->message); g_object_unref (settings); g_error_free (error); return FALSE; } parent = NULL; widget = gtk_widget_get_toplevel (GTK_WIDGET (folder_tree)); if (widget && gtk_widget_is_toplevel (widget) && GTK_IS_WINDOW (widget)) parent = GTK_WINDOW (widget); widget = e_alert_dialog_new_for_args ( parent, is_move ? "mail:ask-folder-drop-move" : "mail:ask-folder-drop-copy", src_folder_name, des_full_name && *des_full_name ? des_full_name : camel_service_get_display_name (CAMEL_SERVICE (des_store)), NULL); response = gtk_dialog_run (GTK_DIALOG (widget)); gtk_widget_destroy (widget); if (response == GTK_RESPONSE_OK) g_settings_set_string (settings, key, "always"); else if (response == GTK_RESPONSE_CANCEL) g_settings_set_string (settings, key, "never"); g_free (src_folder_name); g_object_unref (settings); return response == GTK_RESPONSE_YES || response == GTK_RESPONSE_OK; } /* Drop handling */ struct _DragDataReceivedAsync { MailMsg base; /* input data */ GdkDragContext *context; /* Only selection->data and selection->length are valid */ GtkSelectionData *selection; EMFolderTree *folder_tree; EMailSession *session; CamelStore *store; gchar *full_name; gchar *dest_folder_uri; guint32 action; guint info; guint move : 1; guint moved : 1; guint aborted : 1; }; static void folder_tree_drop_folder (struct _DragDataReceivedAsync *m) { CamelFolder *folder; CamelStore *parent_store; GCancellable *cancellable; const gchar *folder_name; const gchar *full_name; const guchar *data; data = gtk_selection_data_get_data (m->selection); d (printf (" * Drop folder '%s' onto '%s'\n", data, m->full_name)); cancellable = m->base.cancellable; folder = e_mail_session_uri_to_folder_sync ( m->session, (gchar *) data, 0, cancellable, &m->base.error); if (folder == NULL) return; full_name = camel_folder_get_full_name (folder); parent_store = camel_folder_get_parent_store (folder); em_folder_utils_copy_folders ( parent_store, full_name, m->store, m->full_name ? m->full_name : "", m->move); folder_name = strrchr (full_name, '/'); if (folder_name) folder_name++; else folder_name = full_name; if (m->full_name) { gchar *dest_root_name; dest_root_name = g_strconcat (m->full_name, "/", folder_name, NULL); m->dest_folder_uri = e_mail_folder_uri_build (m->store, dest_root_name); g_free (dest_root_name); } else { m->dest_folder_uri = e_mail_folder_uri_build (m->store, folder_name); } g_object_unref (folder); } static gchar * folder_tree_drop_async__desc (struct _DragDataReceivedAsync *m) { const guchar *data; data = gtk_selection_data_get_data (m->selection); if (m->info == DND_DROP_TYPE_FOLDER) { gchar *folder_name = NULL; gchar *res; e_mail_folder_uri_parse ( CAMEL_SESSION (m->session), (gchar *) data, NULL, &folder_name, NULL); g_return_val_if_fail (folder_name != NULL, NULL); if (m->move) res = g_strdup_printf ( _("Moving folder %s"), folder_name); else res = g_strdup_printf ( _("Copying folder %s"), folder_name); g_free (folder_name); return res; } else { if (m->move) return g_strdup_printf ( _("Moving messages into folder %s"), m->full_name); else return g_strdup_printf ( _("Copying messages into folder %s"), m->full_name); } } static void folder_tree_drop_async__exec (struct _DragDataReceivedAsync *m, GCancellable *cancellable, GError **error) { CamelFolder *folder; /* for types other than folder, we can't drop to the root path */ if (m->info == DND_DROP_TYPE_FOLDER) { /* copy or move (aka rename) a folder */ folder_tree_drop_folder (m); } else if (m->full_name == NULL) { g_set_error ( error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, _("Cannot drop message(s) into toplevel store")); } else if ((folder = camel_store_get_folder_sync ( m->store, m->full_name, 0, cancellable, error))) { switch (m->info) { case DND_DROP_TYPE_UID_LIST: /* import a list of uids from another evo folder */ em_utils_selection_get_uidlist ( m->selection, m->session, folder, m->move, cancellable, error); m->moved = m->move && (!error || !*error); break; case DND_DROP_TYPE_MESSAGE_RFC822: /* import a message/rfc822 stream */ em_utils_selection_get_message (m->selection, folder); break; case DND_DROP_TYPE_TEXT_URI_LIST: /* import an mbox, maildir, or mh folder? */ em_utils_selection_get_urilist (m->selection, folder); break; default: abort (); } g_object_unref (folder); } } static void folder_tree_drop_async__free (struct _DragDataReceivedAsync *m) { if (m->move && m->dest_folder_uri) { GList *selected_list; selected_list = g_list_append (NULL, m->dest_folder_uri); em_folder_tree_set_selected_list (m->folder_tree, selected_list, FALSE); g_list_free (selected_list); } g_object_unref (m->folder_tree); g_object_unref (m->session); g_object_unref (m->context); g_object_unref (m->store); g_free (m->full_name); g_free (m->dest_folder_uri); gtk_selection_data_free (m->selection); } static MailMsgInfo folder_tree_drop_async_info = { sizeof (struct _DragDataReceivedAsync), (MailMsgDescFunc) folder_tree_drop_async__desc, (MailMsgExecFunc) folder_tree_drop_async__exec, (MailMsgDoneFunc) NULL, (MailMsgFreeFunc) folder_tree_drop_async__free }; static void tree_drag_data_action (struct _DragDataReceivedAsync *m) { m->move = m->action == GDK_ACTION_MOVE; mail_msg_unordered_push (m); } static void tree_drag_data_received (GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection, guint info, guint time, EMFolderTree *folder_tree) { GtkTreeViewDropPosition pos; GtkTreeModel *model; GtkTreeView *tree_view; GtkTreePath *dest_path = NULL; EMailSession *session; struct _DragDataReceivedAsync *m; gboolean is_store; CamelStore *store; GtkTreeIter iter; gchar *full_name; tree_view = GTK_TREE_VIEW (folder_tree); model = gtk_tree_view_get_model (tree_view); session = em_folder_tree_get_session (folder_tree); if (!gtk_tree_view_get_dest_row_at_pos (tree_view, x, y, &dest_path, &pos)) return; /* this means we are receiving no data */ if (gtk_selection_data_get_data (selection) == NULL) { gtk_drag_finish (context, FALSE, FALSE, GDK_CURRENT_TIME); gtk_tree_path_free (dest_path); return; } if (gtk_selection_data_get_length (selection) == -1) { gtk_drag_finish (context, FALSE, FALSE, GDK_CURRENT_TIME); gtk_tree_path_free (dest_path); return; } if (!gtk_tree_model_get_iter (model, &iter, dest_path)) { gtk_drag_finish (context, FALSE, FALSE, GDK_CURRENT_TIME); gtk_tree_path_free (dest_path); return; } gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_BOOL_IS_STORE, &is_store, COL_STRING_FULL_NAME, &full_name, -1); /* make sure user isn't try to drop on a placeholder row */ if (full_name == NULL && !is_store) { gtk_drag_finish (context, FALSE, FALSE, GDK_CURRENT_TIME); gtk_tree_path_free (dest_path); return; } if (info == DND_DROP_TYPE_FOLDER && !ask_drop_folder (folder_tree, (const gchar *) gtk_selection_data_get_data (selection), full_name, store, gdk_drag_context_get_selected_action (context) == GDK_ACTION_MOVE)) { gtk_drag_finish (context, FALSE, FALSE, GDK_CURRENT_TIME); gtk_tree_path_free (dest_path); g_free (full_name); return; } m = mail_msg_new (&folder_tree_drop_async_info); m->folder_tree = g_object_ref (folder_tree); m->session = g_object_ref (session); m->context = g_object_ref (context); m->store = g_object_ref (store); m->full_name = full_name; m->dest_folder_uri = NULL; 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); tree_drag_data_action (m); gtk_tree_path_free (dest_path); } static gboolean is_special_local_folder (const gchar *name) { return strcmp (name, "Drafts") == 0 || strcmp (name, "Inbox") == 0 || strcmp (name, "Outbox") == 0 || strcmp (name, "Sent") == 0 || strcmp (name, "Templates") == 0; } static GdkAtom folder_tree_drop_target (EMFolderTree *folder_tree, GdkDragContext *context, GtkTreePath *path, GdkDragAction *actions, GdkDragAction *suggested_action) { EMFolderTreePrivate *p = folder_tree->priv; gchar *dst_full_name = NULL; gchar *src_full_name = NULL; CamelStore *dst_store; CamelStore *src_store = NULL; GdkAtom atom = GDK_NONE; gboolean is_store; GtkTreeModel *model; GtkTreeIter iter; GList *targets; const gchar *uid; gboolean src_is_local; gboolean src_is_vfolder; gboolean dst_is_vfolder; guint32 flags = 0; /* This is a bit of a mess, but should handle all the cases properly */ model = gtk_tree_view_get_model (GTK_TREE_VIEW (folder_tree)); if (!gtk_tree_model_get_iter (model, &iter, path)) return GDK_NONE; /* We may override these further down. */ *actions = gdk_drag_context_get_actions (context); *suggested_action = gdk_drag_context_get_suggested_action (context); gtk_tree_model_get ( model, &iter, COL_BOOL_IS_STORE, &is_store, COL_POINTER_CAMEL_STORE, &dst_store, COL_STRING_FULL_NAME, &dst_full_name, COL_UINT_FLAGS, &flags, -1); uid = camel_service_get_uid (CAMEL_SERVICE (dst_store)); dst_is_vfolder = (g_strcmp0 (uid, E_MAIL_SESSION_VFOLDER_UID) == 0); targets = gdk_drag_context_list_targets (context); /* Check for special destinations */ /* Don't allow copying/moving into the UNMATCHED vfolder. */ if (dst_is_vfolder) if (g_strcmp0 (dst_full_name, CAMEL_UNMATCHED_NAME) == 0) goto done; /* Don't allow copying/moving into a vTrash folder. */ if (g_strcmp0 (dst_full_name, CAMEL_VTRASH_NAME) == 0) goto done; /* Don't allow copying/moving into a vJunk folder. */ if (g_strcmp0 (dst_full_name, CAMEL_VJUNK_NAME) == 0) goto done; if (flags & CAMEL_FOLDER_NOSELECT) goto done; if (p->drag_row) { GtkTreePath *src_path = gtk_tree_row_reference_get_path (p->drag_row); if (src_path) { guint32 src_flags = 0; if (gtk_tree_model_get_iter (model, &iter, src_path)) gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &src_store, COL_STRING_FULL_NAME, &src_full_name, COL_UINT_FLAGS, &src_flags, -1); /* can't dnd onto itself or below itself - bad things happen, * no point dragging to where we were either */ if (gtk_tree_path_compare (path, src_path) == 0 || gtk_tree_path_is_descendant (path, src_path) || (gtk_tree_path_is_ancestor (path, src_path) && gtk_tree_path_get_depth (path) == gtk_tree_path_get_depth (src_path) - 1)) { gtk_tree_path_free (src_path); goto done; } gtk_tree_path_free (src_path); if ((src_flags & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_INBOX || (src_flags & CAMEL_FOLDER_SYSTEM) != 0) { /* allow only copy of the Inbox and other system folders */ GdkAtom xfolder; /* force copy for special local folders */ *suggested_action = GDK_ACTION_COPY; *actions = GDK_ACTION_COPY; xfolder = drop_atoms[DND_DROP_TYPE_FOLDER]; while (targets != NULL) { if (targets->data == (gpointer) xfolder) { atom = xfolder; goto done; } targets = targets->next; } goto done; } } } /* Check for special sources, and vfolder stuff */ if (src_store != NULL && src_full_name != NULL) { uid = camel_service_get_uid (CAMEL_SERVICE (src_store)); src_is_local = (g_strcmp0 (uid, E_MAIL_SESSION_LOCAL_UID) == 0); src_is_vfolder = (g_strcmp0 (uid, E_MAIL_SESSION_VFOLDER_UID) == 0); /* FIXME: this is a total hack, but i think all we can do at present */ /* Check for dragging from special folders which can't be moved/copied */ /* Don't allow moving any of the the special local folders. */ if (src_is_local && is_special_local_folder (src_full_name)) { GdkAtom xfolder; /* force copy for special local folders */ *suggested_action = GDK_ACTION_COPY; *actions = GDK_ACTION_COPY; xfolder = drop_atoms[DND_DROP_TYPE_FOLDER]; while (targets != NULL) { if (targets->data == (gpointer) xfolder) { atom = xfolder; goto done; } targets = targets->next; } goto done; } /* Don't allow copying/moving the UNMATCHED vfolder. */ if (src_is_vfolder) if (g_strcmp0 (src_full_name, CAMEL_UNMATCHED_NAME) == 0) goto done; /* Don't allow copying/moving any vTrash folder. */ if (g_strcmp0 (src_full_name, CAMEL_VTRASH_NAME) == 0) goto done; /* Don't allow copying/moving any vJunk folder. */ if (g_strcmp0 (src_full_name, CAMEL_VJUNK_NAME) == 0) goto done; /* Don't allow copying/moving any maildir 'inbox'. */ if (g_strcmp0 (src_full_name, ".") == 0) goto done; /* Search Folders can only be dropped into other * Search Folders. */ if (src_is_vfolder) { /* force move only for vfolders */ *suggested_action = GDK_ACTION_MOVE; if (dst_is_vfolder) { GdkAtom xfolder; xfolder = drop_atoms[DND_DROP_TYPE_FOLDER]; while (targets != NULL) { if (targets->data == (gpointer) xfolder) { atom = xfolder; goto done; } targets = targets->next; } } goto done; } } /* Can't drag anything but a Search Folder into a Search Folder. */ if (dst_is_vfolder) goto done; /* Now we either have a store or a normal folder. */ if (is_store) { GdkAtom xfolder; xfolder = drop_atoms[DND_DROP_TYPE_FOLDER]; while (targets != NULL) { if (targets->data == (gpointer) xfolder) { atom = xfolder; goto done; } targets = targets->next; } } else { gint i; while (targets != NULL) { for (i = 0; i < NUM_DROP_TYPES; i++) { if (targets->data == (gpointer) drop_atoms[i]) { atom = drop_atoms[i]; goto done; } } targets = targets->next; } } done: g_free (dst_full_name); g_free (src_full_name); return atom; } static gboolean tree_drag_drop (GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeViewColumn *column; GtkTreeView *tree_view; gint cell_x, cell_y; GdkDragAction actions; GdkDragAction suggested_action; GtkTreePath *path; GdkAtom target; tree_view = GTK_TREE_VIEW (folder_tree); if (priv->autoscroll_id != 0) { g_source_remove (priv->autoscroll_id); priv->autoscroll_id = 0; } if (priv->autoexpand_id != 0) { gtk_tree_row_reference_free (priv->autoexpand_row); priv->autoexpand_row = NULL; g_source_remove (priv->autoexpand_id); priv->autoexpand_id = 0; } if (!gtk_tree_view_get_path_at_pos ( tree_view, x, y, &path, &column, &cell_x, &cell_y)) return FALSE; target = folder_tree_drop_target ( folder_tree, context, path, &actions, &suggested_action); gtk_tree_path_free (path); return (target != GDK_NONE); } static void tree_drag_end (GtkWidget *widget, GdkDragContext *context, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; if (priv->drag_row != NULL) { gtk_tree_row_reference_free (priv->drag_row); priv->drag_row = NULL; } /* FIXME: undo anything done in drag-begin */ } static void tree_drag_leave (GtkWidget *widget, GdkDragContext *context, guint time, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeView *tree_view; tree_view = GTK_TREE_VIEW (folder_tree); if (priv->autoscroll_id != 0) { g_source_remove (priv->autoscroll_id); priv->autoscroll_id = 0; } if (priv->autoexpand_id != 0) { gtk_tree_row_reference_free (priv->autoexpand_row); priv->autoexpand_row = NULL; g_source_remove (priv->autoexpand_id); priv->autoexpand_id = 0; } gtk_tree_view_set_drag_dest_row ( tree_view, NULL, GTK_TREE_VIEW_DROP_BEFORE); } #define SCROLL_EDGE_SIZE 15 static gboolean tree_autoscroll (EMFolderTree *folder_tree) { GtkAdjustment *adjustment; GtkTreeView *tree_view; GtkScrollable *scrollable; GdkRectangle rect; GdkWindow *window; GdkDisplay *display; GdkDeviceManager *device_manager; GdkDevice *device; gdouble value; gint offset, y; /* Get the y pointer position relative to the treeview. */ tree_view = GTK_TREE_VIEW (folder_tree); window = gtk_tree_view_get_bin_window (tree_view); display = gdk_window_get_display (window); device_manager = gdk_display_get_device_manager (display); device = gdk_device_manager_get_client_pointer (device_manager); gdk_window_get_device_position (window, device, NULL, &y, NULL); /* Rect is in coorinates relative to the scrolled window, * relative to the treeview. */ gtk_tree_view_get_visible_rect (tree_view, &rect); /* Move y into the same coordinate system as rect. */ y += rect.y; /* See if we are near the top edge. */ offset = y - (rect.y + 2 * SCROLL_EDGE_SIZE); if (offset > 0) { /* See if we are near the bottom edge. */ offset = y - (rect.y + rect.height - 2 * SCROLL_EDGE_SIZE); if (offset < 0) return TRUE; } scrollable = GTK_SCROLLABLE (folder_tree); adjustment = gtk_scrollable_get_vadjustment (scrollable); value = gtk_adjustment_get_value (adjustment); gtk_adjustment_set_value (adjustment, MAX (value + offset, 0.0)); return TRUE; } static gboolean tree_autoexpand (EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeView *tree_view; GtkTreePath *path; tree_view = GTK_TREE_VIEW (folder_tree); path = gtk_tree_row_reference_get_path (priv->autoexpand_row); gtk_tree_view_expand_row (tree_view, path, FALSE); gtk_tree_path_free (path); return TRUE; } static gboolean tree_drag_motion (GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, EMFolderTree *folder_tree) { EMFolderTreePrivate *priv = folder_tree->priv; GtkTreeViewDropPosition pos; GtkTreeView *tree_view; GtkTreeModel *model; GdkDragAction actions; GdkDragAction suggested_action; GdkDragAction chosen_action = 0; GtkTreePath *path = NULL; GtkTreeIter iter; GdkAtom target; gint i; tree_view = GTK_TREE_VIEW (folder_tree); model = gtk_tree_view_get_model (tree_view); if (!gtk_tree_view_get_dest_row_at_pos (tree_view, x, y, &path, &pos)) return FALSE; if (priv->autoscroll_id == 0) priv->autoscroll_id = g_timeout_add ( 150, (GSourceFunc) tree_autoscroll, folder_tree); gtk_tree_model_get_iter (model, &iter, path); if (gtk_tree_model_iter_has_child (model, &iter) && !gtk_tree_view_row_expanded (tree_view, path)) { if (priv->autoexpand_id != 0) { GtkTreePath *autoexpand_path; autoexpand_path = gtk_tree_row_reference_get_path ( priv->autoexpand_row); if (gtk_tree_path_compare (autoexpand_path, path) != 0) { /* row changed, restart timer */ gtk_tree_row_reference_free (priv->autoexpand_row); priv->autoexpand_row = gtk_tree_row_reference_new (model, path); g_source_remove (priv->autoexpand_id); priv->autoexpand_id = g_timeout_add ( 600, (GSourceFunc) tree_autoexpand, folder_tree); } gtk_tree_path_free (autoexpand_path); } else { priv->autoexpand_id = g_timeout_add ( 600, (GSourceFunc) tree_autoexpand, folder_tree); priv->autoexpand_row = gtk_tree_row_reference_new (model, path); } } else if (priv->autoexpand_id != 0) { gtk_tree_row_reference_free (priv->autoexpand_row); priv->autoexpand_row = NULL; g_source_remove (priv->autoexpand_id); priv->autoexpand_id = 0; } target = folder_tree_drop_target ( folder_tree, context, path, &actions, &suggested_action); for (i = 0; target != GDK_NONE && i < NUM_DROP_TYPES; i++) { if (drop_atoms[i] != target) continue; switch (i) { case DND_DROP_TYPE_UID_LIST: case DND_DROP_TYPE_FOLDER: chosen_action = suggested_action; if (chosen_action == GDK_ACTION_COPY && (actions & GDK_ACTION_MOVE)) chosen_action = GDK_ACTION_MOVE; gtk_tree_view_set_drag_dest_row ( tree_view, path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER); break; default: gtk_tree_view_set_drag_dest_row ( tree_view, path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER); chosen_action = suggested_action; break; } break; } gdk_drag_status (context, chosen_action, time); gtk_tree_path_free (path); return chosen_action != 0; } void em_folder_tree_enable_drag_and_drop (EMFolderTree *folder_tree) { GtkTreeView *tree_view; static gint setup = 0; gint i; g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); tree_view = GTK_TREE_VIEW (folder_tree); if (!setup) { for (i = 0; i < NUM_DRAG_TYPES; i++) drag_atoms[i] = gdk_atom_intern (drag_types[i].target, FALSE); for (i = 0; i < NUM_DROP_TYPES; i++) drop_atoms[i] = gdk_atom_intern (drop_types[i].target, FALSE); setup = 1; } gtk_drag_source_set ( GTK_WIDGET (tree_view), GDK_BUTTON1_MASK,drag_types, NUM_DRAG_TYPES, GDK_ACTION_COPY | GDK_ACTION_MOVE); gtk_drag_dest_set ( GTK_WIDGET (tree_view), GTK_DEST_DEFAULT_ALL, drop_types, NUM_DROP_TYPES, GDK_ACTION_COPY | GDK_ACTION_MOVE); g_signal_connect ( tree_view, "drag-begin", G_CALLBACK (tree_drag_begin), folder_tree); g_signal_connect ( tree_view, "drag-data-get", G_CALLBACK (tree_drag_data_get), folder_tree); g_signal_connect ( tree_view, "drag-data-received", G_CALLBACK (tree_drag_data_received), folder_tree); g_signal_connect ( tree_view, "drag-drop", G_CALLBACK (tree_drag_drop), folder_tree); g_signal_connect ( tree_view, "drag-end", G_CALLBACK (tree_drag_end), folder_tree); g_signal_connect ( tree_view, "drag-leave", G_CALLBACK (tree_drag_leave), folder_tree); g_signal_connect ( tree_view, "drag-motion", G_CALLBACK (tree_drag_motion), folder_tree); } void em_folder_tree_set_excluded (EMFolderTree *folder_tree, guint32 flags) { g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); folder_tree->priv->excluded = flags; } void em_folder_tree_set_excluded_func (EMFolderTree *folder_tree, EMFTExcludeFunc exclude, gpointer data) { g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); g_return_if_fail (exclude != NULL); folder_tree->priv->excluded_func = exclude; folder_tree->priv->excluded_data = data; } GList * em_folder_tree_get_selected_uris (EMFolderTree *folder_tree) { GtkTreeSelection *selection; GtkTreeView *tree_view; GtkTreeModel *model; GList *list = NULL, *rows, *l; GSList *sl; tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); /* at first, add lost uris */ for (sl = folder_tree->priv->select_uris; sl; sl = g_slist_next (sl)) { const gchar *uri; uri = ((struct _selected_uri *) sl->data)->uri; list = g_list_append (list, g_strdup (uri)); } rows = gtk_tree_selection_get_selected_rows (selection, &model); for (l = rows; l; l = g_list_next (l)) { GtkTreeIter iter; GtkTreePath *path = l->data; if (gtk_tree_model_get_iter (model, &iter, path)) { CamelStore *store; gchar *folder_name; gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_FULL_NAME, &folder_name, -1); if (CAMEL_IS_STORE (store) && folder_name != NULL) { gchar *folder_uri; folder_uri = e_mail_folder_uri_build ( store, folder_name); list = g_list_prepend (list, folder_uri); } g_free (folder_name); } gtk_tree_path_free (path); } g_list_free (rows); return g_list_reverse (list); } static void get_selected_uris_path_iterate (GtkTreeModel *model, GtkTreePath *treepath, GtkTreeIter *iter, gpointer data) { GList **list = (GList **) data; gchar *full_name; gtk_tree_model_get (model, iter, COL_STRING_FULL_NAME, &full_name, -1); *list = g_list_append (*list, full_name); } GList * em_folder_tree_get_selected_paths (EMFolderTree *folder_tree) { GtkTreeSelection *selection; GtkTreeView *tree_view; GList *list = NULL; tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); gtk_tree_selection_selected_foreach ( selection, get_selected_uris_path_iterate, &list); return list; } void em_folder_tree_set_selected_list (EMFolderTree *folder_tree, GList *list, gboolean expand_only) { EMFolderTreePrivate *priv = folder_tree->priv; EMailSession *session; session = em_folder_tree_get_session (folder_tree); /* FIXME: need to remove any currently selected stuff? */ if (!expand_only) folder_tree_clear_selected_list (folder_tree); for (; list; list = list->next) { CamelStore *store; struct _selected_uri *u; const gchar *folder_uri; const gchar *uid; gchar *folder_name; gchar *expand_key; gchar *end; gboolean success; /* This makes sure all our parents up to the root are * expanded. */ folder_uri = list->data; success = e_mail_folder_uri_parse ( CAMEL_SESSION (session), folder_uri, &store, &folder_name, NULL); if (!success) continue; uid = camel_service_get_uid (CAMEL_SERVICE (store)); expand_key = g_strdup_printf ("%s/%s", uid, folder_name); g_free (folder_name); u = g_malloc0 (sizeof (*u)); u->uri = g_strdup (folder_uri); u->service = CAMEL_SERVICE (store); /* takes ownership */ u->key = g_strdup (expand_key); if (!expand_only) { g_hash_table_insert ( priv->select_uris_table, u->key, u); priv->select_uris = g_slist_append (priv->select_uris, u); } end = strrchr (expand_key, '/'); do { folder_tree_expand_node (expand_key, folder_tree); *end = 0; end = strrchr (expand_key, '/'); } while (end); if (expand_only) folder_tree_free_select_uri (u); g_free (expand_key); } } #if 0 static void dump_fi (CamelFolderInfo *fi, gint depth) { gint i; while (fi != NULL) { for (i = 0; i < depth; i++) fputs (" ", stdout); printf ("path='%s'; full_name='%s'\n", fi->path, fi->full_name); if (fi->child) dump_fi (fi->child, depth + 1); fi = fi->sibling; } } #endif void em_folder_tree_set_selected (EMFolderTree *folder_tree, const gchar *uri, gboolean expand_only) { GList *l = NULL; g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); if (uri && uri[0]) l = g_list_append (l, (gpointer) uri); em_folder_tree_set_selected_list (folder_tree, l, expand_only); g_list_free (l); } void em_folder_tree_select_next_path (EMFolderTree *folder_tree, gboolean skip_read_folders) { GtkTreeView *tree_view; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter, parent, child; GtkTreePath *current_path, *path = NULL; guint unread = 0; EMFolderTreePrivate *priv = folder_tree->priv; g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { current_path = gtk_tree_model_get_path (model, &iter); do { if (gtk_tree_model_iter_has_child (model, &iter)) { gtk_tree_model_iter_children (model, &child, &iter); path = gtk_tree_model_get_path (model, &child); iter = child; } else { while (1) { gboolean has_parent; has_parent = gtk_tree_model_iter_parent ( model, &parent, &iter); if (gtk_tree_model_iter_next (model, &iter)) { path = gtk_tree_model_get_path (model, &iter); break; } else { if (has_parent) { iter = parent; } else { /* Reached end. Wrapup*/ gtk_tree_model_get_iter_first (model, &iter); path = gtk_tree_model_get_path (model, &iter); break; } } } } gtk_tree_model_get (model, &iter, COL_UINT_UNREAD, &unread, -1); /* TODO : Flags here for better options */ } while (skip_read_folders && unread <=0 && gtk_tree_path_compare (current_path, path)); } if (path) { if (!gtk_tree_view_row_expanded (tree_view, path)) gtk_tree_view_expand_to_path (tree_view, path); gtk_tree_selection_select_path (selection, path); if (!priv->cursor_set) { gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); priv->cursor_set = TRUE; } gtk_tree_view_scroll_to_cell (tree_view, path, NULL, TRUE, 0.5f, 0.0f); } return; } static gboolean folder_tree_descend (GtkTreeModel *model, GtkTreeIter *iter, GtkTreeIter *root) { GtkTreeIter parent; gint n_children; /* Finds the rightmost descendant of the given root. */ if (root == NULL) { n_children = gtk_tree_model_iter_n_children (model, NULL); /* This will invalidate the iterator and return FALSE. */ if (n_children == 0) return gtk_tree_model_get_iter_first (model, iter); gtk_tree_model_iter_nth_child ( model, &parent, NULL, n_children - 1); } else parent = *root; n_children = gtk_tree_model_iter_n_children (model, &parent); while (n_children > 0) { GtkTreeIter child; gtk_tree_model_iter_nth_child ( model, &child, &parent, n_children - 1); parent = child; n_children = gtk_tree_model_iter_n_children (model, &parent); } *iter = parent; return TRUE; } void em_folder_tree_select_prev_path (EMFolderTree *folder_tree, gboolean skip_read_folders) { GtkTreeView *tree_view; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreePath *path = NULL; GtkTreePath *sentinel; GtkTreeIter iter; guint unread = 0; EMFolderTreePrivate *priv = folder_tree->priv; g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); /* Nothing selected means nothing to do. */ if (!gtk_tree_selection_get_selected (selection, &model, &iter)) return; /* This prevents us from looping over the model indefinitely, * looking for unread messages when there are none. */ sentinel = gtk_tree_model_get_path (model, &iter); do { GtkTreeIter descendant; if (path != NULL) gtk_tree_path_free (path); path = gtk_tree_model_get_path (model, &iter); if (gtk_tree_path_prev (path)) { gtk_tree_model_get_iter (model, &iter, path); folder_tree_descend (model, &descendant, &iter); gtk_tree_path_free (path); path = gtk_tree_model_get_path (model, &descendant); } else if (gtk_tree_path_get_depth (path) > 1) { gtk_tree_path_up (path); } else { folder_tree_descend (model, &descendant, NULL); gtk_tree_path_free (path); path = gtk_tree_model_get_path (model, &descendant); } gtk_tree_model_get_iter (model, &iter, path); gtk_tree_model_get (model, &iter, COL_UINT_UNREAD, &unread, -1); } while (skip_read_folders && unread <= 0 && gtk_tree_path_compare (path, sentinel) != 0); if (!gtk_tree_view_row_expanded (tree_view, path)) gtk_tree_view_expand_to_path (tree_view, path); gtk_tree_selection_select_path (selection, path); if (!priv->cursor_set) { gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); priv->cursor_set = TRUE; } gtk_tree_view_scroll_to_cell ( tree_view, path, NULL, TRUE, 0.5f, 0.0f); gtk_tree_path_free (sentinel); gtk_tree_path_free (path); } void em_folder_tree_edit_selected (EMFolderTree *folder_tree) { GtkTreeSelection *selection; GtkTreeViewColumn *column; GtkCellRenderer *renderer; GtkTreeView *tree_view; GtkTreeModel *model; GtkTreePath *path = NULL; GtkTreeIter iter; g_return_if_fail (EM_IS_FOLDER_TREE (folder_tree)); tree_view = GTK_TREE_VIEW (folder_tree); column = gtk_tree_view_get_column (tree_view, 0); selection = gtk_tree_view_get_selection (tree_view); renderer = folder_tree->priv->text_renderer; if (gtk_tree_selection_get_selected (selection, &model, &iter)) path = gtk_tree_model_get_path (model, &iter); if (path == NULL) return; /* Make the text cell renderer editable, but only temporarily. * We don't want editing to be activated by simply clicking on * the folder name. Too easy for accidental edits to occur. */ g_object_set (renderer, "editable", TRUE, NULL); gtk_tree_view_expand_to_path (tree_view, path); gtk_tree_view_set_cursor_on_cell ( tree_view, path, column, renderer, TRUE); g_object_set (renderer, "editable", FALSE, NULL); gtk_tree_path_free (path); } gboolean em_folder_tree_get_selected (EMFolderTree *folder_tree, CamelStore **out_store, gchar **out_folder_name) { GtkTreeView *tree_view; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; CamelStore *store = NULL; gchar *folder_name = NULL; g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), FALSE); tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); if (!gtk_tree_selection_get_selected (selection, &model, &iter)) return FALSE; gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_FULL_NAME, &folder_name, -1); /* We should always get a valid store. */ g_return_val_if_fail (CAMEL_IS_STORE (store), FALSE); /* If a store is selected, the folder name will be NULL. * Treat this as though nothing is selected, so that callers * can assume a TRUE return value means a folder is selected. */ if (folder_name == NULL) return FALSE; /* FIXME We really should be storing the CamelStore as a GObject * so it gets referenced. The pointer type is a relic of * days before Camel used GObject. */ if (out_store != NULL) *out_store = g_object_ref (store); if (out_folder_name != NULL) *out_folder_name = folder_name; else g_free (folder_name); return TRUE; } gboolean em_folder_tree_store_root_selected (EMFolderTree *folder_tree, CamelStore **out_store) { GtkTreeView *tree_view; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; CamelStore *store = NULL; gboolean is_store = FALSE; g_return_val_if_fail (folder_tree != NULL, FALSE); g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), FALSE); tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); if (!gtk_tree_selection_get_selected (selection, &model, &iter)) return FALSE; gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_BOOL_IS_STORE, &is_store, -1); /* We should always get a valid store. */ g_return_val_if_fail (CAMEL_IS_STORE (store), FALSE); if (!is_store) return FALSE; if (out_store != NULL) *out_store = g_object_ref (store); return TRUE; } gchar * em_folder_tree_get_selected_uri (EMFolderTree *folder_tree) { GtkTreeView *tree_view; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; CamelStore *store; gchar *folder_name; gchar *folder_uri = NULL; g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), NULL); tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); if (!gtk_tree_selection_get_selected (selection, &model, &iter)) return NULL; gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, COL_STRING_FULL_NAME, &folder_name, -1); if (CAMEL_IS_STORE (store) && folder_name != NULL) folder_uri = e_mail_folder_uri_build (store, folder_name); else if (CAMEL_IS_STORE (store)) folder_uri = e_mail_folder_uri_build (store, ""); g_free (folder_name); return folder_uri; } CamelStore * em_folder_tree_get_selected_store (EMFolderTree *folder_tree) { GtkTreeView *tree_view; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; CamelStore *store = NULL; g_return_val_if_fail (EM_IS_FOLDER_TREE (folder_tree), NULL); /* Don't use em_folder_tree_get_selected() here because we * want this to work whether a folder or store is selected. */ tree_view = GTK_TREE_VIEW (folder_tree); selection = gtk_tree_view_get_selection (tree_view); if (gtk_tree_selection_get_selected (selection, &model, &iter)) gtk_tree_model_get ( model, &iter, COL_POINTER_CAMEL_STORE, &store, -1); return CAMEL_IS_STORE (store) ? store : NULL; } void em_folder_tree_set_skip_double_click (EMFolderTree *folder_tree, gboolean skip) { folder_tree->priv->skip_double_click = skip; } /* stores come first, then by uri */ static gint sort_by_store_and_uri (gconstpointer name1, gconstpointer name2) { const gchar *n1 = name1, *n2 = name2; gboolean is_store1, is_store2; if (n1 == NULL || n2 == NULL) { if (n1 == n2) return 0; else return n1 ? -1 : 1; } is_store1 = g_str_has_prefix (n1, "Store "); is_store2 = g_str_has_prefix (n2, "Store "); if ((is_store1 || is_store2) && (!is_store1 || !is_store2)) { return is_store1 ? -1 : 1; } return strcmp (n1, n2); } /* restores state of a tree (collapsed/expanded) as stores in the given key_file */ void em_folder_tree_restore_state (EMFolderTree *folder_tree, GKeyFile *key_file) { EMFolderTreeModel *folder_tree_model; EMailSession *session; GtkTreeModel *tree_model; GtkTreeView *tree_view; GtkTreeIter iter; gboolean valid; gchar **groups_arr; GSList *groups, *group; gint ii; /* Make sure we have a key file to restore state from. */ if (key_file == NULL) return; tree_view = GTK_TREE_VIEW (folder_tree); tree_model = gtk_tree_view_get_model (tree_view); folder_tree_model = EM_FOLDER_TREE_MODEL (tree_model); session = em_folder_tree_model_get_session (folder_tree_model); g_return_if_fail (E_IS_MAIL_SESSION (session)); /* Set the initial folder tree expanded state in two stages: * * 1) Iterate over the "Store" and "Folder" state file groups * and apply the "Expanded" keys where possible. * * 2) Iterate over the top-level nodes in the folder tree * (these are all stores) and expand those that have no * corresponding "Expanded" key in the state file. This * ensures that new stores are expanded by default. */ /* Stage 1 */ /* Collapse all so we have a clean slate. */ gtk_tree_view_collapse_all (tree_view); groups_arr = g_key_file_get_groups (key_file, NULL); groups = NULL; for (ii = 0; groups_arr[ii] != NULL; ii++) { groups = g_slist_prepend (groups, groups_arr[ii]); } groups = g_slist_sort (groups, sort_by_store_and_uri); for (group = groups; group != NULL; group = group->next) { GtkTreeRowReference *reference = NULL; CamelStore *store = NULL; const gchar *group_name = group->data; const gchar *key = STATE_KEY_EXPANDED; gchar *folder_name = NULL; gboolean expanded = FALSE; gboolean success = FALSE; if (g_str_has_prefix (group_name, "Store ")) { CamelService *service; const gchar *uid = group_name + 6; service = camel_session_ref_service ( CAMEL_SESSION (session), uid); if (CAMEL_IS_STORE (service)) { store = g_object_ref (service); success = TRUE; } if (service != NULL) g_object_unref (service); expanded = TRUE; } else if (g_str_has_prefix (group_name, "Folder ")) { const gchar *uri = group_name + 7; success = e_mail_folder_uri_parse ( CAMEL_SESSION (session), uri, &store, &folder_name, NULL); expanded = FALSE; } if (g_key_file_has_key (key_file, group_name, key, NULL)) expanded = g_key_file_get_boolean ( key_file, group_name, key, NULL); if (expanded && success) { EMFolderTreeModelStoreInfo *si; si = em_folder_tree_model_lookup_store_info ( folder_tree_model, store); if (si != NULL) { if (folder_name != NULL) reference = g_hash_table_lookup ( si->full_hash, folder_name); else reference = si->row; } } if (gtk_tree_row_reference_valid (reference)) { GtkTreePath *path; GtkTreeIter iter; path = gtk_tree_row_reference_get_path (reference); gtk_tree_model_get_iter (tree_model, &iter, path); gtk_tree_view_expand_row (tree_view, path, FALSE); gtk_tree_path_free (path); } if (store != NULL) g_object_unref (store); g_free (folder_name); } g_slist_free (groups); g_strfreev (groups_arr); /* Stage 2 */ valid = gtk_tree_model_get_iter_first (tree_model, &iter); while (valid) { CamelStore *store; CamelService *service; const gchar *key = STATE_KEY_EXPANDED; const gchar *uid; gchar *group_name; gtk_tree_model_get ( tree_model, &iter, COL_POINTER_CAMEL_STORE, &store, -1); if (!CAMEL_IS_STORE (store)) goto next; service = CAMEL_SERVICE (store); uid = camel_service_get_uid (service); group_name = g_strdup_printf ("Store %s", uid); /* Expand stores that have no "Expanded" key. */ if (!g_key_file_has_key (key_file, group_name, key, NULL)) { GtkTreePath *path; path = gtk_tree_model_get_path (tree_model, &iter); gtk_tree_view_expand_row (tree_view, path, FALSE); gtk_tree_path_free (path); } g_free (group_name); next: valid = gtk_tree_model_iter_next (tree_model, &iter); } }