Closes #3773. Moving and copying emails between folders is now supported.

This commit is contained in:
Nate Lillich 2012-05-22 13:44:57 -07:00
parent 026f048672
commit cb2f78748b
18 changed files with 512 additions and 135 deletions

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="16" height="16">
<path fill="#797979" d="M2.5,6l5,5l5,-5z"/>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="8" height="16">
<path d="M 0.625,4.8813569 4.0317797,13 7.438559,4.8813569 z" style="fill:#999999" />
</svg>

Before

Width:  |  Height:  |  Size: 288 B

After

Width:  |  Height:  |  Size: 229 B

Before After
Before After

View file

@ -171,6 +171,7 @@ client/ui/geary-login.vala
client/ui/email-entry.vala
client/ui/icon-factory.vala
client/ui/folder-list.vala
client/ui/folder-menu.vala
client/ui/main-toolbar.vala
client/ui/main-window.vala
client/ui/message-list-cell-renderer.vala

View file

@ -52,7 +52,9 @@ public class GearyController {
public const string ACTION_MARK_AS_UNREAD = "GearyMarkAsUnread";
public const string ACTION_MARK_AS_STARRED = "GearyMarkAsStarred";
public const string ACTION_MARK_AS_UNSTARRED = "GearyMarkAsUnStarred";
public const string ACTION_COPY_MENU = "GearyCopyMenuButton";
public const string ACTION_MOVE_MENU = "GearyMoveMenuButton";
private const int FETCH_EMAIL_CHUNK_COUNT = 50;
public MainWindow main_window { get; private set; }
@ -82,17 +84,22 @@ public class GearyController {
main_window = new MainWindow();
enable_message_buttons(false);
// Connect to various UI signals.
main_window.message_list_view.conversations_selected.connect(on_conversations_selected);
main_window.message_list_view.load_more.connect(on_load_more);
main_window.message_list_view.mark_conversation.connect(on_mark_conversation);
main_window.folder_list.folder_selected.connect(on_folder_selected);
main_window.folder_list.copy_conversation.connect(on_copy_conversation);
main_window.folder_list.move_conversation.connect(on_move_conversation);
main_window.main_toolbar.copy_folder_menu.folder_selected.connect(on_copy_conversation);
main_window.main_toolbar.move_folder_menu.folder_selected.connect(on_move_conversation);
main_window.message_viewer.link_selected.connect(on_link_selected);
main_window.message_viewer.reply_to_message.connect(on_reply_to_message);
main_window.message_viewer.reply_all_message.connect(on_reply_all_message);
main_window.message_viewer.forward_message.connect(on_forward_message);
main_window.message_viewer.mark_message.connect(on_message_viewer_mark_message);
main_window.message_list_view.grab_focus();
set_busy(false);
@ -141,8 +148,8 @@ public class GearyController {
quit.label = _("_Quit");
entries += quit;
Gtk.ActionEntry mark_menu = { ACTION_MARK_AS_MENU, null, TRANSLATABLE, null,
null, on_show_mark_menu };
Gtk.ActionEntry mark_menu = { ACTION_MARK_AS_MENU, null, TRANSLATABLE, null, null,
on_show_mark_menu };
mark_menu.label = _("_Mark as...");
entries += mark_menu;
@ -166,6 +173,16 @@ public class GearyController {
mark_unstarred.label = _("U_nstar");
entries += mark_unstarred;
Gtk.ActionEntry copy_menu = { ACTION_COPY_MENU, null, TRANSLATABLE, "L", null,
on_show_copy_menu };
copy_menu.label = _("_Label");
entries += copy_menu;
Gtk.ActionEntry move_menu = { ACTION_MOVE_MENU, null, TRANSLATABLE, "M", null,
on_show_move_menu };
move_menu.label = _("_Move");
entries += move_menu;
Gtk.ActionEntry new_message = { ACTION_NEW_MESSAGE, null, TRANSLATABLE, "<Ctrl>N", null,
on_new_message };
new_message.label = _("_New Message");
@ -595,8 +612,11 @@ public class GearyController {
foreach (Geary.Folder folder in added) {
if (ignored_paths != null && ignored_paths.contains(folder.get_path()))
skipped.add(folder);
else
else {
main_window.folder_list.add_folder(folder);
main_window.main_toolbar.copy_folder_menu.add_folder(folder);
main_window.main_toolbar.move_folder_menu.add_folder(folder);
}
}
Gee.Collection<Geary.Folder> remaining = added;
@ -821,7 +841,51 @@ public class GearyController {
private void on_mark_complete() {
set_busy(false);
}
private void on_show_copy_menu() {
main_window.main_toolbar.copy_folder_menu.show();
}
private void on_show_move_menu() {
main_window.main_toolbar.move_folder_menu.show();
}
private void on_copy_conversation(Geary.Folder destination) {
// Nothing to do if nothing selected.
if (selected_conversations == null || selected_conversations.length == 0) {
return;
}
Gee.List<Geary.EmailIdentifier> ids = get_selected_ids();
if (ids.size > 0) {
set_busy(true);
current_folder.copy_email_async.begin(ids, destination.get_path(), cancellable_message,
on_copy_complete);
}
}
private void on_copy_complete() {
set_busy(false);
}
private void on_move_conversation(Geary.Folder destination) {
// Nothing to do if nothing selected.
if (selected_conversations == null || selected_conversations.length == 0) {
return;
}
Gee.List<Geary.EmailIdentifier> ids = get_selected_ids();
if (ids.size > 0) {
set_busy(true);
current_folder.move_email_async.begin(ids, destination.get_path(), cancellable_message,
on_move_complete);
}
}
private void on_move_complete() {
set_busy(false);
}
// Opens a link in an external browser.
private void open_uri(string _link) {
string link = _link;
@ -965,7 +1029,7 @@ public class GearyController {
open_uri(link);
}
}
// Disables all single-message buttons and enables all multi-message buttons.
public void enable_multiple_message_buttons(){
// Single message only buttons.
@ -976,8 +1040,10 @@ public class GearyController {
// Mutliple message buttons.
GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE).sensitive = true;
GearyApplication.instance.actions.get_action(ACTION_MARK_AS_MENU).sensitive = true;
GearyApplication.instance.actions.get_action(ACTION_COPY_MENU).sensitive = true;
GearyApplication.instance.actions.get_action(ACTION_MOVE_MENU).sensitive = true;
}
// Enables or disables the message buttons on the toolbar.
public void enable_message_buttons(bool sensitive) {
GearyApplication.instance.actions.get_action(ACTION_REPLY_TO_MESSAGE).sensitive = sensitive;
@ -985,6 +1051,8 @@ public class GearyController {
GearyApplication.instance.actions.get_action(ACTION_FORWARD_MESSAGE).sensitive = sensitive;
GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE).sensitive = sensitive;
GearyApplication.instance.actions.get_action(ACTION_MARK_AS_MENU).sensitive = sensitive;
GearyApplication.instance.actions.get_action(ACTION_COPY_MENU).sensitive = sensitive;
GearyApplication.instance.actions.get_action(ACTION_MOVE_MENU).sensitive = sensitive;
}
public void compose_mailto(string mailto) {

View file

@ -5,31 +5,70 @@
*/
public class FolderList : Sidebar.Tree {
public const Gtk.TargetEntry[] TARGET_ENTRY_LIST = {
{ "application/x-geary-mail", Gtk.TargetFlags.SAME_APP, 0 }
};
private class SpecialFolderBranch : Sidebar.RootOnlyBranch {
public SpecialFolderBranch(Geary.SpecialFolder special, Geary.Folder folder) {
base(new SpecialFolderEntry(special, folder));
}
}
private class SpecialFolderEntry : Object, Sidebar.Entry, Sidebar.SelectableEntry {
public Geary.SpecialFolder special { get; private set; }
private class FolderEntry : Object, Sidebar.Entry, Sidebar.InternalDropTargetEntry,
Sidebar.SelectableEntry {
public Geary.Folder folder { get; private set; }
public SpecialFolderEntry(Geary.SpecialFolder special, Geary.Folder folder) {
this.special = special;
public FolderEntry(Geary.Folder folder) {
this.folder = folder;
}
public string get_sidebar_name() {
return special.name;
public virtual string get_sidebar_name() {
return folder.get_path().basename;
}
public string? get_sidebar_tooltip() {
return null;
}
public Icon? get_sidebar_icon() {
public virtual Icon? get_sidebar_icon() {
return IconFactory.instance.label_icon;
}
public virtual string to_string() {
return "FolderEntry: " + get_sidebar_name();
}
public bool internal_drop_received(Gdk.DragContext context, Gtk.SelectionData data) {
// Copy or move?
Gdk.ModifierType mask;
double[] axes = new double[2];
context.get_device().get_state(context.get_dest_window(), axes, out mask);
MainWindow main_window = GearyApplication.instance.get_main_window() as MainWindow;
if ((mask & Gdk.ModifierType.CONTROL_MASK) != 0) {
main_window.folder_list.copy_conversation(folder);
} else {
main_window.folder_list.move_conversation(folder);
}
return true;
}
}
private class SpecialFolderEntry : FolderEntry {
public Geary.SpecialFolder special { get; private set; }
public SpecialFolderEntry(Geary.SpecialFolder special, Geary.Folder folder) {
base (folder);
this.special = special;
}
public override string get_sidebar_name() {
return special.name;
}
public override Icon? get_sidebar_icon() {
switch (special.folder_type) {
case Geary.SpecialFolderType.INBOX:
return new ThemedIcon("mail-inbox");
@ -57,36 +96,14 @@ public class FolderList : Sidebar.Tree {
}
}
public string to_string() {
public override string to_string() {
return "SpecialFolderEntry: " + get_sidebar_name();
}
}
private class FolderEntry : Object, Sidebar.Entry, Sidebar.SelectableEntry {
public Geary.Folder folder { get; private set; }
public FolderEntry(Geary.Folder folder) {
this.folder = folder;
}
public string get_sidebar_name() {
return folder.get_path().basename;
}
public string? get_sidebar_tooltip() {
return null;
}
public Icon? get_sidebar_icon() {
return IconFactory.instance.label_icon;
}
public string to_string() {
return "FolderEntry: " + get_sidebar_name();
}
}
public signal void folder_selected(Geary.Folder? folder);
public signal void copy_conversation(Geary.Folder folder);
public signal void move_conversation(Geary.Folder folder);
private Sidebar.Grouping user_folder_group;
private Sidebar.Branch user_folder_branch;
@ -96,13 +113,17 @@ public class FolderList : Sidebar.Tree {
public FolderList() {
base(new Gtk.TargetEntry[0], Gdk.DragAction.ASK, drop_handler);
entry_selected.connect(on_entry_selected);
user_folder_group = new Sidebar.Grouping("", IconFactory.instance.label_folder_icon);
user_folder_branch = new Sidebar.Branch(user_folder_group,
Sidebar.Branch.Options.STARTUP_OPEN_GROUPING, user_folder_comparator);
graft(user_folder_branch, int.MAX);
// Set self as a drag destination.
Gtk.drag_dest_set(this, Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT,
TARGET_ENTRY_LIST, Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
}
private static int user_folder_comparator(Sidebar.Entry a, Sidebar.Entry b) {
int result = a.get_sidebar_name().collate(b.get_sidebar_name());
@ -120,7 +141,7 @@ public class FolderList : Sidebar.Tree {
folder_selected(((FolderEntry) selectable).folder);
}
}
public void set_user_folders_root_name(string name) {
user_folder_group.rename(name);
}
@ -162,4 +183,20 @@ public class FolderList : Sidebar.Tree {
private Sidebar.Entry? get_entry_for_folder_path(Geary.FolderPath path) {
return entries.get(path);
}
public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) {
// Run the base version first.
bool ret = base.drag_motion(context, x, y, time);
// Update the cursor for copy or move.
Gdk.ModifierType mask;
double[] axes = new double[2];
context.get_device().get_state(context.get_dest_window(), axes, out mask);
if ((mask & Gdk.ModifierType.CONTROL_MASK) != 0) {
Gdk.drag_status(context, Gdk.DragAction.COPY, time);
} else {
Gdk.drag_status(context, Gdk.DragAction.MOVE, time);
}
return ret;
}
}

View file

@ -0,0 +1,78 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class FolderMenu {
private Gtk.Menu? menu = null;
private Gtk.ToggleToolButton button;
private Gee.List<Geary.Folder> folder_list = new Gee.ArrayList<Geary.Folder>();
public signal void folder_selected(Geary.Folder folder);
public FolderMenu(Gtk.ToggleToolButton button) {
this.button = button;
}
public void add_folder(Geary.Folder folder) {
folder_list.add(folder);
folder_list.sort((CompareFunc) folder_sort);
menu = null;
}
public void remove_folder(Geary.Folder folder) {
int index = folder_list.index_of(folder);
if (index >= 0) {
folder_list.remove_at(index);
}
}
public void show() {
// Prevent activation loops.
if (!button.active) {
return;
}
// If the menu is currently null, build it.
if (menu == null) {
build_menu();
}
// Show the menu.
menu.popup(null, null, menu_popup_relative, 0, 0);
menu.select_first(true);
}
private void build_menu() {
// TODO Add fancy filter option.
// TODO Make the menu items checkboxes instead of buttons.
// TODO Merge the move/copy menus and just have a move/copy buttons at bottom of this menu.
menu = new Gtk.Menu();
foreach (Geary.Folder folder in folder_list) {
Gtk.MenuItem menu_item = new Gtk.MenuItem.with_label(folder.get_path().to_string());
menu_item.activate.connect(() => {
on_menu_item_activated(folder);
});
menu.append(menu_item);
}
// Finish setting up the menu.
menu.attach_to_widget(button, null);
menu.deactivate.connect(on_menu_deactivate);
menu.show_all();
}
private void on_menu_deactivate() {
button.active = false;
}
private void on_menu_item_activated(Geary.Folder folder) {
folder_selected(folder);
}
private static int folder_sort(Geary.Folder a, Geary.Folder b) {
return a.get_path().to_string().casefold().collate(b.get_path().to_string().casefold());
}
}

View file

@ -7,95 +7,72 @@
// Draws the main toolbar.
public class MainToolbar : Gtk.Box {
private Gtk.Toolbar toolbar;
private Gtk.Menu menu;
private Gtk.Menu mark_menu;
private Gtk.ToggleToolButton menu_button;
private Gtk.ToggleToolButton mark_menu_button;
public FolderMenu copy_folder_menu { get; private set; }
public FolderMenu move_folder_menu { get; private set; }
public MainToolbar() {
Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
GearyApplication.instance.load_ui_file("toolbar_mark_menu.ui");
mark_menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMarkMenu") as Gtk.Menu;
GearyApplication.instance.load_ui_file("toolbar_menu.ui");
menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMenu") as Gtk.Menu;
Gtk.Builder builder = GearyApplication.instance.create_builder("toolbar.glade");
toolbar = builder.get_object("toolbar") as Gtk.Toolbar;
Gtk.ToolButton new_message = builder.get_object(GearyController.ACTION_NEW_MESSAGE)
as Gtk.ToolButton;
new_message.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_NEW_MESSAGE));
Gtk.ToolButton reply_to_message = builder.get_object(GearyController.ACTION_REPLY_TO_MESSAGE)
as Gtk.ToolButton;
reply_to_message.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_REPLY_TO_MESSAGE));
Gtk.ToolButton reply_all_message = builder.get_object(GearyController.ACTION_REPLY_ALL_MESSAGE)
as Gtk.ToolButton;
reply_all_message.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_REPLY_ALL_MESSAGE));
Gtk.ToolButton forward_message = builder.get_object(GearyController.ACTION_FORWARD_MESSAGE)
as Gtk.ToolButton;
forward_message.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_FORWARD_MESSAGE));
Gtk.ToolButton archive_message = builder.get_object(GearyController.ACTION_DELETE_MESSAGE)
as Gtk.ToolButton;
archive_message.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_DELETE_MESSAGE));
mark_menu_button = builder.get_object(GearyController.ACTION_MARK_AS_MENU) as Gtk.ToggleToolButton;
mark_menu_button.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_MARK_AS_MENU));
mark_menu.attach_to_widget(mark_menu_button, null);
mark_menu.deactivate.connect(on_deactivate_mark_menu);
mark_menu_button.clicked.connect(on_show_mark_menu);
// Setup each of the normal toolbar buttons.
set_toolbutton_action(builder, GearyController.ACTION_NEW_MESSAGE);
set_toolbutton_action(builder, GearyController.ACTION_REPLY_TO_MESSAGE);
set_toolbutton_action(builder, GearyController.ACTION_REPLY_ALL_MESSAGE);
set_toolbutton_action(builder, GearyController.ACTION_FORWARD_MESSAGE);
set_toolbutton_action(builder, GearyController.ACTION_DELETE_MESSAGE);
Gtk.ToggleButton button = mark_menu_button.get_child() as Gtk.ToggleButton;
button.remove(button.get_child());
Gtk.Box box = new Gtk.HBox(false, 0);
button.add(box);
box.pack_start(new Gtk.Label(_("Mark")));
box.pack_start(new Gtk.Image.from_icon_name("menu-down", Gtk.IconSize.LARGE_TOOLBAR));
// Setup the folder menus (move/copy).
Gtk.ToggleToolButton copy_menu_button = set_toolbutton_action(builder,
GearyController.ACTION_COPY_MENU) as Gtk.ToggleToolButton;
make_menu_dropdown_button(copy_menu_button, _("Label as"));
copy_folder_menu = new FolderMenu(copy_menu_button);
Gtk.ToggleToolButton move_menu_button = set_toolbutton_action(builder,
GearyController.ACTION_MOVE_MENU) as Gtk.ToggleToolButton;
make_menu_dropdown_button(move_menu_button, _("Move to"));
move_folder_menu = new FolderMenu(move_menu_button);
// Assemble mark menu button.
GearyApplication.instance.load_ui_file("toolbar_mark_menu.ui");
Gtk.Menu mark_menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMarkMenu")
as Gtk.Menu;
Gtk.ToggleToolButton mark_menu_button = set_toolbutton_action(builder,
GearyController.ACTION_MARK_AS_MENU) as Gtk.ToggleToolButton;
attach_menu(mark_menu, mark_menu_button);
make_menu_dropdown_button(mark_menu_button, _("Mark"));
// Setup the application menu.
GearyApplication.instance.load_ui_file("toolbar_menu.ui");
Gtk.Menu menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMenu") as Gtk.Menu;
attach_menu(menu, builder.get_object("menu_button") as Gtk.ToggleToolButton);
menu_button = builder.get_object("menu_button") as Gtk.ToggleToolButton;
menu.attach_to_widget(menu_button, null);
menu.deactivate.connect(on_deactivate_menu);
menu_button.clicked.connect(on_show_menu);
toolbar.get_style_context().add_class("primary-toolbar");
add(toolbar);
}
private void on_show_menu() {
// Prevent loop
if (!menu_button.active)
return;
menu.popup(null, null, menu_popup_relative, 0, 0);
menu.select_first(true);
private void attach_menu(Gtk.Menu menu, Gtk.ToggleToolButton button) {
menu.attach_to_widget(button, null);
menu.deactivate.connect(() => {
button.active = false;
});
button.clicked.connect(() => {
// Prevent loops.
if (!button.active) {
return;
}
menu.popup(null, null, menu_popup_relative, 0, 0);
menu.select_first(true);
});
}
private void on_deactivate_menu() {
menu_button.active = false;
}
private void on_show_mark_menu() {
// Prevent loop
if (!mark_menu_button.active)
return;
mark_menu.popup(null, null, menu_popup_relative, 0, 0);
mark_menu.select_first(true);
}
private void on_deactivate_mark_menu() {
mark_menu_button.active = false;
private Gtk.ToolButton set_toolbutton_action(Gtk.Builder builder, string action) {
Gtk.ToolButton button = builder.get_object(action) as Gtk.ToolButton;
button.set_related_action(GearyApplication.instance.actions.get_action(action));
return button;
}
}

View file

@ -36,6 +36,10 @@ public class MessageListView : Gtk.TreeView {
store.row_deleted.connect(on_row_deleted);
button_press_event.connect(on_button_press);
// Set up drag and drop.
Gtk.drag_source_set(this, Gdk.ModifierType.BUTTON1_MASK, FolderList.TARGET_ENTRY_LIST,
Gdk.DragAction.COPY | Gdk.DragAction.MOVE);
}
private bool on_button_press(Gdk.EventButton event) {

View file

@ -54,7 +54,7 @@ public interface Sidebar.DestroyableEntry : Sidebar.Entry {
public interface Sidebar.InternalDropTargetEntry : Sidebar.Entry {
// Returns true if drop was successful
public abstract bool internal_drop_received(Gtk.SelectionData data);
public abstract bool internal_drop_received(Gdk.DragContext context, Gtk.SelectionData data);
}
public interface Sidebar.InternalDragSourceEntry : Sidebar.Entry {

View file

@ -1009,7 +1009,7 @@ public class Sidebar.Tree : Gtk.TreeView {
return;
}
bool success = targetable.internal_drop_received(selection_data);
bool success = targetable.internal_drop_received(context, selection_data);
Gtk.drag_finish(context, success, false, time);
}

View file

@ -24,3 +24,13 @@ public void menu_popup_relative(Gtk.Menu menu, out int x, out int y, out bool pu
push_in = false;
}
public void make_menu_dropdown_button(Gtk.ToggleToolButton toolbutton, string label) {
Gtk.ToggleButton button = toolbutton.get_child() as Gtk.ToggleButton;
button.remove(button.get_child());
Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
box.set_homogeneous(false);
button.add(box);
box.pack_start(new Gtk.Label(label));
box.pack_start(new Gtk.Image.from_icon_name("menu-down", Gtk.IconSize.SMALL_TOOLBAR));
}

View file

@ -418,7 +418,23 @@ public interface Geary.Folder : Object {
public abstract async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) throws Error;
/**
* Copies messages into another folder.
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
/**
* Moves messages to another folder.
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
/**
* check_span_specifiers() verifies that the span specifiers match the requirements set by
* list_email_async() and lazy_list_email_async(). If not, this method throws

View file

@ -146,7 +146,24 @@ private class Geary.Imap.Folder : Object {
yield mailbox.mark_email_async(msg_set, msg_flags_add, msg_flags_remove, cancellable);
}
public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
Cancellable? cancellable = null) throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
yield mailbox.copy_email_async(msg_set, destination, cancellable);
}
public async void move_email_async(MessageSet msg_set, Geary.FolderPath destination,
Cancellable? cancellable = null) throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
yield copy_email_async(msg_set, destination, cancellable);
yield remove_email_async(msg_set, cancellable);
}
public string to_string() {
return path.to_string();
}

View file

@ -150,3 +150,15 @@ public class Geary.Imap.IdleCommand : Command {
}
}
public class Geary.Imap.CopyCommand : Command {
public const string NAME = "copy";
public const string UID_NAME = "uid copy";
public CopyCommand(MessageSet message_set, string destination) {
base (message_set.is_uid ? UID_NAME : NAME);
add(message_set.to_parameter());
add(new StringParameter(destination));
}
}

View file

@ -586,7 +586,17 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
}
}
}
public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
Cancellable? cancellable = null) throws Error {
if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
yield context.session.send_command_async(new CopyCommand(msg_set, destination.to_string()),
cancellable);
}
public async void expunge_email_async(MessageSet? msg_set, Cancellable? cancellable = null) throws Error {
if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);

View file

@ -96,7 +96,13 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
public abstract async void mark_email_async(
Gee.List<Geary.EmailIdentifier> to_mark, Geary.EmailFlags? flags_to_add,
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error;
public abstract async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
public abstract async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error;
public virtual string to_string() {
return get_path().to_string();
}

View file

@ -877,13 +877,32 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder {
public override async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) throws Error {
check_open("mark_email_async");
if (!yield wait_for_remote_to_open(cancellable))
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
replay_queue.schedule(new MarkEmail(this, to_mark, flags_to_add, flags_to_remove,
cancellable));
}
public override async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
check_open("copy_email_async");
if (!yield wait_for_remote_to_open(cancellable))
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
replay_queue.schedule(new CopyEmail(this, to_copy, destination));
}
public override async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
check_open("move_email_async");
if (!yield wait_for_remote_to_open(cancellable))
throw new EngineError.SERVER_UNAVAILABLE("No connection to %s", remote.to_string());
replay_queue.schedule(new MoveEmail(this, to_move, destination));
}
private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
notify_email_flags_changed(changed);
}

View file

@ -617,3 +617,96 @@ private class Geary.FetchEmail : Geary.SendReplayOperation {
}
}
private class Geary.CopyEmail : Geary.SendReplayOperation {
private GenericImapFolder engine;
private Gee.List<Geary.EmailIdentifier> to_copy;
private Geary.FolderPath destination;
private Cancellable? cancellable;
public CopyEmail(GenericImapFolder engine, Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) {
base("CopyEmail");
this.engine = engine;
this.to_copy = to_copy;
this.destination = destination;
this.cancellable = cancellable;
}
public override async ReplayOperation.Status replay_local_async() throws Error {
// The local DB will be updated when the remote folder is opened and we see a new message
// existing there.
return ReplayOperation.Status.CONTINUE;
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
yield engine.remote_folder.copy_email_async(new Imap.MessageSet.email_id_collection(to_copy),
destination, cancellable);
return ReplayOperation.Status.COMPLETED;
}
public override async void backout_local_async() throws Error {
// Nothing to undo.
}
public override string describe_state() {
return "%d email IDs to %s".printf(to_copy.size, destination.to_string());
}
}
private class Geary.MoveEmail : Geary.SendReplayOperation {
private GenericImapFolder engine;
private Gee.List<Geary.EmailIdentifier> to_move;
private Geary.FolderPath destination;
private Cancellable? cancellable;
private int original_count = 0;
public MoveEmail(GenericImapFolder engine, Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) {
base("MoveEmail");
this.engine = engine;
this.to_move = to_move;
this.destination = destination;
this.cancellable = cancellable;
}
public override async ReplayOperation.Status replay_local_async() throws Error {
// Remove the email from the folder.
// TODO: Use a local_folder method that operates on all messages at once
foreach (Geary.EmailIdentifier id in to_move)
yield engine.local_folder.mark_removed_async(id, true, cancellable);
engine.notify_email_removed(to_move);
original_count = engine.remote_count;
engine.notify_email_count_changed(original_count - to_move.size,
Geary.Folder.CountChangeReason.REMOVED);
return ReplayOperation.Status.CONTINUE;
}
public override async ReplayOperation.Status replay_remote_async() throws Error {
yield engine.remote_folder.move_email_async(new Imap.MessageSet.email_id_collection(to_move),
destination, cancellable);
return ReplayOperation.Status.COMPLETED;
}
public override async void backout_local_async() throws Error {
// Add the email back in.
// TODO: Use a local_folder method that operates on all messages at once
foreach (Geary.EmailIdentifier id in to_move)
yield engine.local_folder.mark_removed_async(id, false, cancellable);
engine.notify_email_appended(to_move);
engine.notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.ADDED);
}
public override string describe_state() {
return "%d email IDs to %s".printf(to_move.size, destination.to_string());
}
}

View file

@ -94,6 +94,36 @@
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToggleToolButton" id="GearyMoveMenuButton">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes" context="Toggles menu for applying labels to emails.">Move the selected conversation</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">False</property>
<property name="icon_name">folder</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToggleToolButton" id="GearyCopyMenuButton">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes" context="Toggles menu for applying labels to emails.">Label the selected conversation</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">False</property>
<property name="icon_name">multiple-tags</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkSeparatorToolItem" id="separator2">
<property name="use_action_appearance">False</property>