Autosave drafts. Closes #6124
This commit is contained in:
parent
8059f0f31e
commit
f78ddf6bbd
4 changed files with 139 additions and 91 deletions
|
|
@ -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>
|
||||
<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="position">2</property>
|
||||
<property name="non_homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="Discard">
|
||||
<property name="label">_Discard</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">2</property>
|
||||
<property name="secondary">True</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">3</property>
|
||||
<property name="secondary">True</property>
|
||||
</packing>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,4 @@
|
|||
<accelerator action="insertlink" />
|
||||
|
||||
<accelerator action="close" />
|
||||
|
||||
<accelerator action="save" />
|
||||
</ui>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue