Commit f78ddf6b authored by Eric Gregory's avatar Eric Gregory

Autosave drafts. Closes #6124

parent 8059f0f3
......@@ -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 {
</style>
</head><body id="message-body"></body></html>""";
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 <Primary>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<weak Gtk.Action> 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();
}
}
......@@ -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");
}
......@@ -142,12 +142,6 @@
<object class="GtkAction" id="close"/>
<accelerator key="w" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="save">
<property name="label" translatable="yes">Sa_ve Draft</property>
<property name="visible">False</property>
</object>
</child>
</object>
<object class="GtkArrow" id="menu arrow">
<property name="visible">True</property>
......@@ -664,6 +658,7 @@
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
......@@ -680,51 +675,64 @@
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_draft_button">
<property name="related_action">save</property>
<object class="GtkLabel" id="draft_save_label">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="Discard">
<property name="label">_Discard</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">3</property>
<property name="pack_type">end</property>
<property name="position">2</property>
<property name="secondary">True</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="Send">
<property name="label" translatable="yes">_Send</property>
<object class="GtkButtonBox" id="buttonbox1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
<property name="can_focus">False</property>
<property name="spacing">8</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton" id="Close">
<property name="label">_Close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">3</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="Send">
<property name="label" translatable="yes">_Send</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">3</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">3</property>
<property name="pack_type">end</property>
<property name="position">3</property>
<property name="secondary">True</property>
</packing>
......
......@@ -28,6 +28,4 @@
<accelerator action="insertlink" />
<accelerator action="close" />
<accelerator action="save" />
</ui>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment