[geary/bug/728002-webkit2: 79/140] Re-enable composer empty body checking and draft save timer.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/bug/728002-webkit2: 79/140] Re-enable composer empty body checking and draft save timer.
- Date: Tue, 31 Jan 2017 23:05:11 +0000 (UTC)
commit fcf5be297e26a2c9d6bf81380baa4a0de6c01374
Author: Michael James Gratton <mike vee net>
Date: Fri Jan 6 10:48:40 2017 +1100
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.
src/client/application/geary-controller.vala | 2 +-
src/client/composer/composer-web-view.vala | 36 ++++++++--
src/client/composer/composer-widget.vala | 96 +++++++++++--------------
ui/composer-web-view.js | 38 +++++------
4 files changed, 92 insertions(+), 80 deletions(-)
---
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 0e7ae7e..fcde502 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -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 {
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index 2b3a4cd..08a0dc0 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -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();
+ }
}
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 87c13a4..f95b91d 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -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;
+ }
+ }
+
+ /** Determines if current message can be saved as draft. */
+ private bool can_save {
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.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?
}
diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js
index 98324d1..ae39790 100644
--- a/ui/composer-web-view.js
+++ b/ui/composer-web-view.js
@@ -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);
},
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]