Re-enable composer empty body checking and draft save timer.
* src/client/composer/composer-web-view.vala (ClientWebView): Add a ::document_modified signal and a documentModified JS message listener, fire it when the JS message is receieved. Update value of ::is_empty based on whether a non-empty HTML body was provided in the first place, and if it has been subsequently modified. Update related doc comments a bit. * src/client/composer/composer-widget.vala (ComposerWidget): Rename `blank` property to `is_blank`, fix sense of editor.is_blank check, update call sites. Convert ::can_save method into a property, include the this.is_blank check since there's no point saving a blank message, updtae call sites. Replace use of GLib.Timeout with Geary.TimeoutManager, tidy up resulting code, hook up timer to new document_modified signal. * ui/composer-web-view.js: Use body mutation observer to send documentModified messages to the client, coalescing consecutive events over a period of 1s into a single message.
This commit is contained in:
parent
8d479cf437
commit
fcf5be297e
4 changed files with 92 additions and 80 deletions
|
|
@ -2403,7 +2403,7 @@ public class GearyController : Geary.BaseObject {
|
|||
if (compose_type == ComposerWidget.ComposeType.NEW_MESSAGE) {
|
||||
foreach (ComposerWidget cw in composer_widgets) {
|
||||
if (cw.state == ComposerWidget.ComposerState.NEW) {
|
||||
if (!cw.blank) {
|
||||
if (!cw.is_blank) {
|
||||
inline = false;
|
||||
return true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public class ComposerWebView : ClientWebView {
|
|||
|
||||
private const string COMMAND_STACK_CHANGED = "commandStackChanged";
|
||||
private const string CURSOR_STYLE_CHANGED = "cursorStyleChanged";
|
||||
private const string DOCUMENT_MODIFIED = "documentModified";
|
||||
|
||||
private const string[] SANS_FAMILY_NAMES = {
|
||||
"sans", "arial", "trebuchet", "helvetica"
|
||||
|
|
@ -96,14 +97,24 @@ public class ComposerWebView : ClientWebView {
|
|||
);
|
||||
}
|
||||
|
||||
/** Determines if the view contains any edited text */
|
||||
public bool is_empty { get; private set; default = false; }
|
||||
/**
|
||||
* Determines if the body contains any non-boilerplate content.
|
||||
*
|
||||
* Currently, only a signatures are considered to be boilerplate.
|
||||
* Any user-made changes or message body content from a
|
||||
* forwarded/replied-to message present will make the view
|
||||
* considered to be non-empty.
|
||||
*/
|
||||
public bool is_empty { get; private set; default = true; }
|
||||
|
||||
/** Determines if the view is in rich text mode */
|
||||
/** Determines if the view is in rich text mode. */
|
||||
public bool is_rich_text { get; private set; default = true; }
|
||||
|
||||
|
||||
/** Emitted when the web view's undo/redo stack has changed. */
|
||||
/** Emitted when the web view's content has changed. */
|
||||
public signal void document_modified();
|
||||
|
||||
/** Emitted when the web view's undo/redo stack state changes. */
|
||||
public signal void command_stack_changed(bool can_undo, bool can_redo);
|
||||
|
||||
/** Emitted when the style under the cursor has changed. */
|
||||
|
|
@ -124,9 +135,13 @@ public class ComposerWebView : ClientWebView {
|
|||
this.user_content_manager.script_message_received[CURSOR_STYLE_CHANGED].connect(
|
||||
on_cursor_style_changed_message
|
||||
);
|
||||
this.user_content_manager.script_message_received[DOCUMENT_MODIFIED].connect(
|
||||
on_document_modified_message
|
||||
);
|
||||
|
||||
register_message_handler(COMMAND_STACK_CHANGED);
|
||||
register_message_handler(CURSOR_STYLE_CHANGED);
|
||||
register_message_handler(DOCUMENT_MODIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,7 +151,8 @@ public class ComposerWebView : ClientWebView {
|
|||
string html = "";
|
||||
signature = signature ?? "";
|
||||
|
||||
if (body == null)
|
||||
this.is_empty = Geary.String.is_empty(body);
|
||||
if (this.is_empty)
|
||||
html = CURSOR + "<br /><br />" + signature;
|
||||
else if (top_posting)
|
||||
html = CURSOR + "<br /><br />" + signature + body;
|
||||
|
|
@ -404,4 +420,14 @@ public class ComposerWebView : ClientWebView {
|
|||
}
|
||||
}
|
||||
|
||||
private void on_document_modified_message(WebKit.JavascriptResult result) {
|
||||
result.unref();
|
||||
|
||||
// Only modify actually changed to avoid excessive notify
|
||||
// signals being fired.
|
||||
if (this.is_empty) {
|
||||
this.is_empty = false;
|
||||
}
|
||||
document_modified();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,9 +155,7 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
|
||||
private const string URI_LIST_MIME_TYPE = "text/uri-list";
|
||||
private const string FILE_URI_PREFIX = "file://";
|
||||
|
||||
private const int DRAFT_TIMEOUT_SEC = 10;
|
||||
|
||||
|
||||
public const string ATTACHMENT_KEYWORDS_SUFFIX = ".doc|.pdf|.xls|.ppt|.rtf|.pps";
|
||||
|
||||
// A list of keywords, separated by pipe ("|") characters, that suggest an attachment; since
|
||||
|
|
@ -204,15 +202,26 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
|
||||
public Gee.Set<Geary.EmailIdentifier> referred_ids = new Gee.HashSet<Geary.EmailIdentifier>();
|
||||
|
||||
public bool blank {
|
||||
/** Determines if the composer is completely empty. */
|
||||
public bool is_blank {
|
||||
get {
|
||||
return this.to_entry.empty &&
|
||||
this.cc_entry.empty &&
|
||||
this.bcc_entry.empty &&
|
||||
this.reply_to_entry.empty &&
|
||||
this.subject_entry.buffer.length == 0 &&
|
||||
!this.editor.is_empty &&
|
||||
this.attached_files.size == 0;
|
||||
return this.to_entry.empty
|
||||
&& this.cc_entry.empty
|
||||
&& this.bcc_entry.empty
|
||||
&& this.reply_to_entry.empty
|
||||
&& this.subject_entry.buffer.length == 0
|
||||
&& this.editor.is_empty
|
||||
&& this.attached_files.size == 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Determines if current message can be saved as draft. */
|
||||
private bool can_save {
|
||||
get {
|
||||
return this.draft_manager != null
|
||||
&& this.draft_manager.is_open
|
||||
&& this.account.information.save_drafts
|
||||
&& !this.is_blank;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +345,9 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
|
||||
private Geary.App.DraftManager? draft_manager = null;
|
||||
private Geary.EmailFlags draft_flags = new Geary.EmailFlags.with(Geary.EmailFlags.DRAFT);
|
||||
private uint draft_save_timeout_id = 0;
|
||||
private Geary.TimeoutManager draft_timer;
|
||||
|
||||
// Is the composer closing (e.g. saving a draft or sending)?
|
||||
private bool is_closing = false;
|
||||
|
||||
private ComposerContainer container {
|
||||
|
|
@ -462,6 +473,10 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
update_signature();
|
||||
update_pending_attachments(this.pending_include, true);
|
||||
|
||||
this.draft_timer = new Geary.TimeoutManager.seconds(
|
||||
10, () => { this.save_draft.begin(); }
|
||||
);
|
||||
|
||||
// Add actions once every element has been initialized and added
|
||||
initialize_actions();
|
||||
|
||||
|
|
@ -476,12 +491,12 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.editor.command_stack_changed.connect(on_command_state_changed);
|
||||
this.editor.context_menu.connect(on_context_menu);
|
||||
this.editor.cursor_style_changed.connect(on_cursor_style_changed);
|
||||
this.editor.document_modified.connect(() => { draft_changed(); });
|
||||
this.editor.get_editor_state().notify["typing-attributes"].connect(on_typing_attributes_changed);
|
||||
this.editor.key_press_event.connect(on_editor_key_press_event);
|
||||
this.editor.load_changed.connect(on_load_changed);
|
||||
this.editor.mouse_target_changed.connect(on_mouse_target_changed);
|
||||
this.editor.selection_changed.connect(on_selection_changed);
|
||||
//this.editor.user_changed_contents.connect(reset_draft_timer);
|
||||
|
||||
this.editor.load_html(this.body_html, this.signature_html, this.top_posting);
|
||||
|
||||
|
|
@ -1034,23 +1049,16 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.to_entry.addresses);
|
||||
this.to_entry.modified = this.cc_entry.modified = false;
|
||||
}
|
||||
|
||||
|
||||
in_reply_to.add(referred.message_id);
|
||||
referred_ids.add(referred.id);
|
||||
}
|
||||
|
||||
private bool can_save() {
|
||||
return this.draft_manager != null
|
||||
&& this.draft_manager.is_open
|
||||
&& this.editor.is_empty
|
||||
&& this.account.information.save_drafts;
|
||||
}
|
||||
|
||||
public CloseStatus should_close() {
|
||||
if (this.is_closing)
|
||||
return CloseStatus.PENDING_CLOSE;
|
||||
|
||||
bool try_to_save = can_save();
|
||||
bool try_to_save = this.can_save;
|
||||
|
||||
this.container.present();
|
||||
AlertDialog dialog;
|
||||
|
|
@ -1085,7 +1093,7 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
}
|
||||
|
||||
private void on_close_and_save(SimpleAction action, Variant? param) {
|
||||
if (can_save())
|
||||
if (this.can_save)
|
||||
save_and_exit_async.begin();
|
||||
else
|
||||
on_close(action, param);
|
||||
|
|
@ -1339,37 +1347,17 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
debug("Draft manager closed");
|
||||
}
|
||||
|
||||
// Resets the draft save timeout.
|
||||
private void reset_draft_timer() {
|
||||
private inline void draft_changed() {
|
||||
this.draft_save_text = "";
|
||||
cancel_draft_timer();
|
||||
|
||||
if (can_save())
|
||||
draft_save_timeout_id = Timeout.add_seconds(DRAFT_TIMEOUT_SEC, on_save_draft_timeout);
|
||||
}
|
||||
|
||||
// Cancels the draft save timeout
|
||||
private void cancel_draft_timer() {
|
||||
if (this.draft_save_timeout_id == 0)
|
||||
return;
|
||||
|
||||
Source.remove(this.draft_save_timeout_id);
|
||||
this.draft_save_timeout_id = 0;
|
||||
}
|
||||
|
||||
private bool on_save_draft_timeout() {
|
||||
// this is not rescheduled by the event loop, so kill the timeout id
|
||||
this.draft_save_timeout_id = 0;
|
||||
|
||||
save_draft.begin();
|
||||
|
||||
return false;
|
||||
if (this.can_save) {
|
||||
this.draft_timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Note that drafts are NOT "linkified."
|
||||
private async void save_draft() {
|
||||
// cancel timer in favor of just doing it now
|
||||
cancel_draft_timer();
|
||||
this.draft_timer.reset();
|
||||
|
||||
if (this.draft_manager != null) {
|
||||
try {
|
||||
|
|
@ -1385,8 +1373,8 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
|
||||
private Geary.Nonblocking.Semaphore? discard_draft() {
|
||||
// cancel timer in favor of this operation
|
||||
cancel_draft_timer();
|
||||
|
||||
this.draft_timer.reset();
|
||||
|
||||
try {
|
||||
if (this.draft_manager != null)
|
||||
return this.draft_manager.discard();
|
||||
|
|
@ -1400,7 +1388,7 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
// Used while waiting for draft to save before closing widget.
|
||||
private void make_gui_insensitive() {
|
||||
this.container.vanish();
|
||||
cancel_draft_timer();
|
||||
this.draft_timer.reset();
|
||||
}
|
||||
|
||||
private async void save_and_exit_async() {
|
||||
|
|
@ -1598,7 +1586,7 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
|
||||
[GtkCallback]
|
||||
private void on_subject_changed() {
|
||||
reset_draft_timer();
|
||||
draft_changed();
|
||||
}
|
||||
|
||||
private void validate_send_button() {
|
||||
|
|
@ -1627,7 +1615,7 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.header.set_recipients(label, tooltip.str.slice(0, -1)); // Remove trailing \n
|
||||
}
|
||||
|
||||
reset_draft_timer();
|
||||
draft_changed();
|
||||
}
|
||||
|
||||
private void on_justify(SimpleAction action, Variant? param) {
|
||||
|
|
@ -2130,7 +2118,7 @@ public class ComposerWidget : Gtk.EventBox {
|
|||
this.open_draft_manager_async.begin(null, null, (obj, res) => {
|
||||
try {
|
||||
this.open_draft_manager_async.end(res);
|
||||
reset_draft_timer();
|
||||
draft_changed();
|
||||
} catch (Error e) {
|
||||
// Oh well?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,15 @@ ComposerPageState.prototype = {
|
|||
}
|
||||
}, true);
|
||||
|
||||
let modifiedId = null;
|
||||
this.bodyObserver = new MutationObserver(function() {
|
||||
state.checkCommandStack();
|
||||
if (modifiedId == null) {
|
||||
modifiedId = window.setTimeout(function() {
|
||||
state.documentModified();
|
||||
state.checkCommandStack();
|
||||
modifiedId = null;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
},
|
||||
loaded: function() {
|
||||
|
|
@ -88,7 +95,13 @@ ComposerPageState.prototype = {
|
|||
// Enable editing and observation machinery only after
|
||||
// modifying the body above.
|
||||
this.messageBody.contentEditable = true;
|
||||
this.setBodyObserverEnabled(true);
|
||||
let config = {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
};
|
||||
this.bodyObserver.observe(this.messageBody, config);
|
||||
|
||||
// Chain up here so we continue to a preferred size update
|
||||
// after munging the HTML above.
|
||||
|
|
@ -133,28 +146,10 @@ ComposerPageState.prototype = {
|
|||
document.body.classList.add("plain");
|
||||
}
|
||||
},
|
||||
setBodyObserverEnabled: function(enabled) {
|
||||
if (enabled) {
|
||||
let config = {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
};
|
||||
this.bodyObserver.observe(this.messageBody, config);
|
||||
} else {
|
||||
this.bodyObserver.disconnect();
|
||||
}
|
||||
},
|
||||
checkCommandStack: function() {
|
||||
let canUndo = document.queryCommandEnabled("undo");
|
||||
let canRedo = document.queryCommandEnabled("redo");
|
||||
|
||||
// Update the body observer - if we can undo we don't need to
|
||||
// keep an eye on mutations any more, until we can't undo
|
||||
// again.
|
||||
this.setBodyObserverEnabled(!canUndo);
|
||||
|
||||
if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) {
|
||||
this.undoEnabled = canUndo;
|
||||
this.redoEnabled = canRedo;
|
||||
|
|
@ -173,6 +168,9 @@ ComposerPageState.prototype = {
|
|||
element.setAttribute("type", "cite");
|
||||
}
|
||||
},
|
||||
documentModified: function(element) {
|
||||
window.webkit.messageHandlers.documentModified.postMessage(null);
|
||||
},
|
||||
linkClicked: function(element) {
|
||||
window.getSelection().selectAllChildren(element);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue