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