diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index e3fc6e80..4a5171c1 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -14,6 +14,9 @@ public class ComposerWindow : Gtk.Window { } private const string DEFAULT_TITLE = _("New Message"); + private const string DRAFT_SAVED_TEXT = _("Saved"); + private const string DRAFT_SAVING_TEXT = _("Saving draft..."); + private const string DRAFT_ERROR_TEXT = _("Error saving draft"); private const string ACTION_UNDO = "undo"; private const string ACTION_REDO = "redo"; @@ -38,7 +41,6 @@ public class ComposerWindow : Gtk.Window { private const string ACTION_INSERT_LINK = "insertlink"; private const string ACTION_COMPOSE_AS_HTML = "compose as html"; private const string ACTION_CLOSE = "close"; - private const string ACTION_SAVE = "save"; private const string URI_LIST_MIME_TYPE = "text/uri-list"; private const string FILE_URI_PREFIX = "file://"; @@ -81,6 +83,8 @@ public class ComposerWindow : Gtk.Window { """; + private const int DRAFT_TIMEOUT_MSEC = 2000; // 2 seconds + public const string ATTACHMENT_KEYWORDS_GENERIC = ".doc|.pdf|.xls|.ppt|.rtf|.pps"; /// A list of keywords, separated by pipe ("|") characters, that suggest an attachment public const string ATTACHMENT_KEYWORDS_LOCALIZED = _("attach|enclosed|enclosing|cover letter"); @@ -144,7 +148,7 @@ public class ComposerWindow : Gtk.Window { private EmailEntry cc_entry; private EmailEntry bcc_entry; private Gtk.Entry subject_entry; - private Gtk.Button discard_button; + private Gtk.Button close_button; private Gtk.Button send_button; private Gtk.ToggleToolButton menu_button; private Gtk.Label message_overlay_label; @@ -156,6 +160,7 @@ public class ComposerWindow : Gtk.Window { private Gtk.Alignment visible_on_attachment_drag_over; private Gtk.Widget hidden_on_attachment_drag_over_child; private Gtk.Widget visible_on_attachment_drag_over_child; + private Gtk.Label draft_save_label; private Gtk.Menu menu_html; private Gtk.Menu menu_plain; @@ -174,8 +179,8 @@ public class ComposerWindow : Gtk.Window { private Geary.FolderSupport.Create? drafts_folder = null; private Geary.EmailIdentifier? draft_id = null; + private uint draft_save_timeout_id = 0; private Cancellable cancellable_drafts = new Cancellable(); - private string default_save_label = ""; private WebKit.WebView editor; // We need to keep a reference to the edit-fixer in composer-window, so it doesn't get @@ -198,8 +203,8 @@ public class ComposerWindow : Gtk.Window { button_area.get_style_context().add_class("content-view"); Gtk.Box box = builder.get_object("composer") as Gtk.Box; - discard_button = builder.get_object("Discard") as Gtk.Button; - discard_button.clicked.connect(on_discard); + close_button = builder.get_object("Close") as Gtk.Button; + close_button.clicked.connect(on_close); send_button = builder.get_object("Send") as Gtk.Button; send_button.clicked.connect(on_send); add_attachment_button = builder.get_object("add_attachment_button") as Gtk.Button; @@ -227,6 +232,7 @@ public class ComposerWindow : Gtk.Window { set_entry_completions(); subject_entry = builder.get_object("subject") as Gtk.Entry; Gtk.Alignment message_area = builder.get_object("message area") as Gtk.Alignment; + draft_save_label = (Gtk.Label) builder.get_object("draft_save_label"); actions = builder.get_object("compose actions") as Gtk.ActionGroup; // Can only happen after actions exits compose_as_html = GearyApplication.instance.config.compose_as_html; @@ -287,8 +293,6 @@ public class ComposerWindow : Gtk.Window { actions.get_action(ACTION_CLOSE).activate.connect(on_close); - actions.get_action(ACTION_SAVE).activate.connect(on_save); - ui = new Gtk.UIManager(); ui.insert_action_group(actions, 0); add_accel_group(ui.get_accel_group()); @@ -369,6 +373,7 @@ public class ComposerWindow : Gtk.Window { editor.redo.connect(update_actions); editor.selection_changed.connect(update_actions); editor.key_press_event.connect(on_key_press); + editor.user_changed_contents.connect(reset_draft_timer); // only do this after setting body_html editor.load_string(HTML_BODY, "text/html", "UTF8", ""); @@ -451,9 +456,10 @@ public class ComposerWindow : Gtk.Window { chain.append(button_area); box.set_focus_chain(chain); - actions.get_action(ACTION_SAVE).sensitive = false; - default_save_label = actions.get_action(ACTION_SAVE).label; - open_drafts_folder.begin(cancellable_drafts); // Open drafts folder for initial account. + // If there's only one account, open the drafts folder. If there's more than one account, + // the drafts folder will be opened by on_from_changed(). + if (!from_multiple.visible) + open_drafts_folder.begin(cancellable_drafts); } public ComposerWindow.from_mailto(Geary.Account account, string mailto) { @@ -671,18 +677,19 @@ public class ComposerWindow : Gtk.Window { _("Do you want to discard the unsaved message?"), null, Stock._DISCARD); } else { dialog = new TernaryConfirmationDialog(this, - _("Do you want to save this message to your Drafts folder?"), null, - Stock._SAVE, Stock._DISCARD, Gtk.ResponseType.CLOSE); + _("Do you want to discard this message?"), null, Stock._KEEP, Stock._DISCARD, + Gtk.ResponseType.CLOSE); } Gtk.ResponseType response = dialog.run(); - if (response == Gtk.ResponseType.CANCEL) { + if (response == Gtk.ResponseType.CANCEL || response == Gtk.ResponseType.DELETE_EVENT) { return false; // Cancel } else if (response == Gtk.ResponseType.OK) { save_and_exit.begin(); // Save return false; } else { - return true; // Discard + delete_and_exit.begin(); // Discard + return false; } } @@ -690,17 +697,11 @@ public class ComposerWindow : Gtk.Window { return !should_close(); } - private void on_discard() { + private void on_close() { if (should_close()) destroy(); } - private void on_close() { - // Accelerator w was pressed to close the composer window. Do the same as - // when clicking the Discard button, at least for now. - on_discard(); - } - private bool email_contains_attachment_keywords() { // Filter out all content contained in block quotes string filtered = @"$subject\n"; @@ -789,25 +790,12 @@ public class ComposerWindow : Gtk.Window { warning("Error sending email: %s", e.message); } - // If there's a draft, delete it. - Geary.FolderSupport.Remove? removable_drafts = drafts_folder as Geary.FolderSupport.Remove; - try { - if (draft_id != null && removable_drafts != null) - yield removable_drafts.remove_single_email_async(draft_id); - } catch (Error e) { - warning("Unable to delete draft: %s", e.message); - } + yield delete_draft_async(); } // Returns the drafts folder for the current From account. private async void open_drafts_folder(Cancellable cancellable) throws Error { - if (drafts_folder != null) { - // Close existing folder. - yield drafts_folder.close_async(cancellable); - drafts_folder = null; - } - - actions.get_action(ACTION_SAVE).sensitive = false; + yield close_drafts_folder(cancellable); Geary.FolderSupport.Create? folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS) as Geary.FolderSupport.Create; @@ -817,50 +805,57 @@ public class ComposerWindow : Gtk.Window { yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN, cancellable); - // Only show Save button if we have a drafts folder to write to. - actions.get_action(ACTION_SAVE).sensitive = true; - drafts_folder = folder; } + private async void close_drafts_folder(Cancellable? cancellable = null) throws Error { + if (drafts_folder == null) + return; + + // Close existing folder. + yield drafts_folder.close_async(cancellable); + drafts_folder = null; + } + // Save to the draft folder, if available. // Note that drafts are NOT "linkified." - private void on_save() { - save_async.begin(on_save_completed); + private bool save_draft() { + save_async.begin(); + + return false; } private async void save_async() { - if (drafts_folder == null) { - warning("No drafts folder available for this account."); - + if (drafts_folder == null) return; - } - actions.get_action(ACTION_SAVE).sensitive = false; - actions.get_action(ACTION_SAVE).set_label(_("Saving...")); + draft_save_label.label = DRAFT_SAVING_TEXT; + draft_save_timeout_id = 0; try { draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email( get_composed_email()), new Geary.EmailFlags(), null, draft_id, null); + + draft_save_label.label = DRAFT_SAVED_TEXT; } catch (Error e) { warning("Error saving draft: %s", e.message); + draft_save_label.label = DRAFT_ERROR_TEXT; } } - private void on_save_completed() { - actions.get_action(ACTION_SAVE).sensitive = true; - actions.get_action(ACTION_SAVE).set_label(default_save_label); - } - // Prevents user from editing anything. Used while waiting for draft to save before exiting window. private void make_gui_insensitive() { + // Halt draft timer. + if (draft_save_timeout_id != 0) + Source.remove(draft_save_timeout_id); + // Disable all actions. List actions = actions.list_actions(); foreach (Gtk.Action a in actions) a.sensitive = false; // Disable buttons. - discard_button.sensitive = send_button.sensitive = menu_button.sensitive = + close_button.sensitive = send_button.sensitive = menu_button.sensitive = add_attachment_button.sensitive = pending_attachments_button.sensitive = false; // Disable editable widgets. @@ -878,6 +873,34 @@ public class ComposerWindow : Gtk.Window { destroy(); } + private async void delete_and_exit() { + delayed_close = true; + make_gui_insensitive(); + + // Do the delete. + yield delete_draft_async(); + + destroy(); + } + + private async void delete_draft_async(Cancellable? cancellable = null) { + if (drafts_folder == null || draft_id == null) + return; + + Geary.FolderSupport.Remove? removable_drafts = drafts_folder as Geary.FolderSupport.Remove; + if (removable_drafts == null) { + warning("Draft folder does not support remove.\n"); + + return; + } + + try { + yield removable_drafts.remove_single_email_async(draft_id); + } catch (Error e) { + warning("Unable to delete draft: %s", e.message); + } + } + private void on_add_attachment_button_clicked() { AttachmentDialog dialog = null; do { @@ -1007,12 +1030,16 @@ public class ComposerWindow : Gtk.Window { private void on_subject_changed() { title = Geary.String.is_empty(subject_entry.text.strip()) ? DEFAULT_TITLE : subject_entry.text.strip(); + + reset_draft_timer(); } private void validate_send_button() { send_button.sensitive = to_entry.valid_or_empty && cc_entry.valid_or_empty && bcc_entry.valid_or_empty && (!to_entry.empty || !cc_entry.empty || !bcc_entry.empty); + + reset_draft_timer(); } private void on_formatting_action(Gtk.Action action) { @@ -1506,6 +1533,16 @@ public class ComposerWindow : Gtk.Window { return false; } + // Resets the draft save timeout. + private void reset_draft_timer() { + draft_save_label.label = ""; + if (draft_save_timeout_id != 0) + Source.remove(draft_save_timeout_id); + + if (drafts_folder != null) + draft_save_timeout_id = Timeout.add(DRAFT_TIMEOUT_MSEC, save_draft); + } + private void update_actions() { // Undo/redo. actions.get_action(ACTION_UNDO).sensitive = editor.can_undo(); @@ -1611,8 +1648,6 @@ public class ComposerWindow : Gtk.Window { if (compose_type != ComposeType.NEW_MESSAGE) return; - actions.get_action(ACTION_SAVE).sensitive = false; - // Since we've set the combo box ID to the email addresses, we can // fetch that and use it to grab the account from the engine. string? id = from_multiple.get_active_id(); @@ -1632,6 +1667,8 @@ public class ComposerWindow : Gtk.Window { debug("Error updating account in Composer: %s", e.message); } } + + reset_draft_timer(); } private void set_entry_completions() { @@ -1644,5 +1681,9 @@ public class ComposerWindow : Gtk.Window { cc_entry.completion = new ContactEntryCompletion(contact_list_store); bcc_entry.completion = new ContactEntryCompletion(contact_list_store); } + + public override void destroy() { + close_drafts_folder.begin(); + } } diff --git a/src/client/ui/stock.vala b/src/client/ui/stock.vala index a17bf5e6..a79d4c3b 100644 --- a/src/client/ui/stock.vala +++ b/src/client/ui/stock.vala @@ -30,6 +30,7 @@ public const string _QUIT = _("_Quit"); public const string _REMOVE = _("_Remove"); public const string _SAVE = _("_Save"); public const string SELECT__ALL = _("Select _All"); +public const string _KEEP = _("_Keep"); } diff --git a/ui/composer.glade b/ui/composer.glade index 7f32f323..33fac03f 100644 --- a/ui/composer.glade +++ b/ui/composer.glade @@ -142,12 +142,6 @@ - - - Sa_ve Draft - False - - True @@ -664,6 +658,7 @@ False False 0 + True @@ -680,51 +675,64 @@ False False 1 + True - - save + True - True - True + False + 0 False True 2 + True - - _Discard + True - True - True - True + False + 8 + start + + + _Close + True + True + True + True + + + False + True + 3 + end + 0 + + + + + _Send + True + True + True + True + + + False + True + 3 + end + 1 + + False True - 3 - end - 2 - True - - - - - _Send - True - True - True - True - - - False - True - 3 - end 3 True diff --git a/ui/composer_accelerators.ui b/ui/composer_accelerators.ui index 4bb8ce0f..eaa3022b 100644 --- a/ui/composer_accelerators.ui +++ b/ui/composer_accelerators.ui @@ -28,6 +28,4 @@ - -