Reimplement loading and cleaning message into WK2 composer web view.

* src/client/application/geary-controller.vala (GearyController::open_async):
  Load ComposerWebView resources.

* src/client/composer/composer-web-view.vala (ComposerWebView): Move
  HTML/CSS template here from ComposerWidget. Load composer-web-view.js
  on app init and add it to the web view's user content manager.
  (ComposerWebView::load_html): Overridden to require HTML body and
  signature, assemble complete HTML as appropriate before chaining up to
  the default impl.
  (ComposerWebView::load_finished_and_realised): Remove redundant method.

* src/client/composer/composer-widget.vala (ComposerWidget):
  Remove 'message' prop since it is unused and onerous. Cache current
  account's signature as a field so it can be passed through to the
  editor as needed. Port on_link_clicked to composer-web-view.js.

* src/client/web-process/util-composer.vala: Remove function ported to JS
  in composer-web-view.js

* ui/CMakeLists.txt: Include new ComposerWebView JS resource.

* ui/composer-web-view.js: Port composer HTML sanitisation methods to JS,
  add to a custom subclass of PageState. Instantiate it and hook it up to
  onload.
This commit is contained in:
Michael James Gratton 2016-11-30 22:56:01 +11:00
parent b02059795f
commit 3f90f7785a
6 changed files with 181 additions and 187 deletions

View file

@ -210,6 +210,7 @@ public class GearyController : Geary.BaseObject {
// Load web view resources
try {
ClientWebView.load_scripts(this.application);
ComposerWebView.load_resources(this.application);
ConversationWebView.load_resources(this.application);
} catch (Error err) {
error("Error loading web resources: %s", err.message);

View file

@ -12,6 +12,61 @@
public class ComposerWebView : ClientWebView {
private const string HTML_BODY = """
<html><head><title></title>
<style>
body {
margin: 0px !important;
padding: 0 !important;
background-color: white !important;
font-size: medium !important;
}
body.plain, body.plain * {
font-family: monospace !important;
font-weight: normal;
font-style: normal;
font-size: medium !important;
color: black;
text-decoration: none;
}
body.plain a {
cursor: text;
}
#message-body {
box-sizing: border-box;
padding: 10px;
outline: 0px solid transparent;
min-height: 100%;
}
blockquote {
margin-top: 0px;
margin-bottom: 0px;
margin-left: 10px;
margin-right: 10px;
padding-left: 5px;
padding-right: 5px;
background-color: white;
border: 0;
border-left: 3px #aaa solid;
}
pre {
white-space: pre-wrap;
margin: 0;
}
</style>
</head><body>
<div id="message-body" contenteditable="true" dir="auto">%s</div>
</body></html>""";
private const string CURSOR = "<span id=\"cursormarker\"></span>";
private static WebKit.UserScript? app_script = null;
public static void load_resources(GearyApplication app)
throws Error {
ComposerWebView.app_script =
ClientWebView.load_app_script(app, "composer-web-view.js");
}
private bool is_shift_down = false;
@ -19,6 +74,9 @@ public class ComposerWebView : ClientWebView {
public ComposerWebView() {
base();
this.user_content_manager.add_script(ComposerWebView.app_script);
get_editor_state().notify["typing-attributes"].connect(() => {
text_attributes_changed(get_editor_state().typing_attributes);
});
@ -27,6 +85,23 @@ public class ComposerWebView : ClientWebView {
this.key_press_event.connect(on_key_press_event);
}
/**
* Loads a message HTML body into the view.
*/
public new void load_html(string? body, string? signature, bool top_posting) {
string html = "";
signature = signature ?? "";
if (body == null)
html = CURSOR + "<br /><br />" + signature;
else if (top_posting)
html = CURSOR + "<br /><br />" + signature + body;
else
html = body + CURSOR + "<br /><br />" + signature;
base.load_html(HTML_BODY.printf(html), null);
}
public bool can_undo() {
// can_execute_editing_command.begin(
// WebKit.EDITING_COMMAND_UNDO,
@ -137,13 +212,6 @@ public class ComposerWebView : ClientWebView {
return ""; // XXX
}
/**
* ???
*/
public void load_finished_and_realised() {
// XXX
}
/**
* ???
*/

View file

@ -151,52 +151,6 @@ public class ComposerWidget : Gtk.EventBox {
private const string URI_LIST_MIME_TYPE = "text/uri-list";
private const string FILE_URI_PREFIX = "file://";
private const string HTML_BODY = """
<html><head><title></title>
<style>
body {
margin: 0px !important;
padding: 0 !important;
background-color: white !important;
font-size: medium !important;
}
body.plain, body.plain * {
font-family: monospace !important;
font-weight: normal;
font-style: normal;
font-size: medium !important;
color: black;
text-decoration: none;
}
body.plain a {
cursor: text;
}
#message-body {
box-sizing: border-box;
padding: 10px;
outline: 0px solid transparent;
min-height: 100%;
}
blockquote {
margin-top: 0px;
margin-bottom: 0px;
margin-left: 10px;
margin-right: 10px;
padding-left: 5px;
padding-right: 5px;
background-color: white;
border: 0;
border-left: 3px #aaa solid;
}
pre {
white-space: pre-wrap;
margin: 0;
}
</style>
</head><body>
<div id="message-body" contenteditable="true" dir="auto"></div>
</body></html>""";
private const string CURSOR = "<span id=\"cursormarker\"></span>";
private const int DRAFT_TIMEOUT_SEC = 10;
@ -240,14 +194,6 @@ public class ComposerWidget : Gtk.EventBox {
set { this.subject_entry.set_text(value); }
}
public string message {
owned get { return get_html(); }
set {
this.body_html = value;
this.editor.load_html(HTML_BODY, null);
}
}
public ComposerState state { get; internal set; }
public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; }
@ -286,6 +232,7 @@ public class ComposerWidget : Gtk.EventBox {
private ContactListStore? contact_list_store = null;
private string? body_html = null;
private string? signature_html = null;
[GtkChild]
private Gtk.Box composer_container;
@ -489,14 +436,9 @@ public class ComposerWidget : Gtk.EventBox {
}
update_from_field();
update_signature();
update_pending_attachments(this.pending_include, true);
// only add signature if the option is actually set and if this is not a draft
if (this.account.information.use_email_signature && !is_referred_draft)
add_signature_and_cursor();
else
set_cursor();
// Add actions once every element has been initialized and added
initialize_actions();
@ -522,8 +464,7 @@ public class ComposerWidget : Gtk.EventBox {
this.editor.key_press_event.connect(on_editor_key_press);
//this.editor.user_changed_contents.connect(reset_draft_timer);
// only do this after setting body_html
this.editor.load_html(HTML_BODY, null);
this.editor.load_html(this.body_html, this.signature_html, this.top_posting);
GearyApplication.instance.config.settings.changed[Configuration.SPELL_CHECK_KEY].connect(
on_spell_check_changed);
@ -850,10 +791,6 @@ public class ComposerWidget : Gtk.EventBox {
// This is safe to call even when this connection hasn't been made.
realize.disconnect(on_load_finished_and_realized);
if (!Geary.String.is_empty(this.body_html)) {
this.editor.load_finished_and_realised();
}
on_spell_check_changed();
update_actions();
@ -1089,55 +1026,6 @@ public class ComposerWidget : Gtk.EventBox {
referred_ids.add(referred.id);
}
private void add_signature_and_cursor() {
string? signature = null;
// If use signature is enabled but no contents are on settings then we'll use ~/.signature, if any
// otherwise use whatever the user has input in settings dialog
if (this.account.information.use_email_signature
&& Geary.String.is_empty_or_whitespace(this.account.information.email_signature)) {
File signature_file = File.new_for_path(Environment.get_home_dir()).get_child(".signature");
if (!signature_file.query_exists()) {
set_cursor();
return;
}
try {
FileUtils.get_contents(signature_file.get_path(), out signature);
if (Geary.String.is_empty_or_whitespace(signature)) {
set_cursor();
return;
}
signature = Geary.HTML.smart_escape(signature, false);
} catch (Error error) {
debug("Error reading signature file %s: %s", signature_file.get_path(), error.message);
set_cursor();
return;
}
} else {
signature = account.information.email_signature;
if (Geary.String.is_empty_or_whitespace(signature)) {
set_cursor();
return;
}
signature = Geary.HTML.smart_escape(signature, true);
}
if (this.body_html == null)
this.body_html = CURSOR + "<br /><br />" + signature;
else if (top_posting)
this.body_html = CURSOR + "<br /><br />" + signature + this.body_html;
else
this.body_html = this.body_html + CURSOR + "<br /><br />" + signature;
}
private void set_cursor() {
if (top_posting)
this.body_html = CURSOR + this.body_html;
else
this.body_html = this.body_html + CURSOR;
}
private bool can_save() {
return this.draft_manager != null
&& this.draft_manager.is_open
@ -2054,11 +1942,7 @@ public class ComposerWidget : Gtk.EventBox {
this.can_delete_quote = false;
if (event.keyval == Gdk.Key.BackSpace) {
this.body_html = null;
if (this.account.information.use_email_signature)
add_signature_and_cursor();
else
set_cursor();
this.editor.load_html(HTML_BODY, null);
this.editor.load_html(this.body_html, this.signature_html, this.top_posting);
return true;
}
}
@ -2256,11 +2140,42 @@ public class ComposerWidget : Gtk.EventBox {
return false;
this.account = new_account;
update_signature();
load_entry_completions.begin();
return true;
}
private void update_signature() {
string? account_sig = null;
if (this.account.information.use_email_signature) {
account_sig = account.information.email_signature;
if (Geary.String.is_empty_or_whitespace(account_sig)) {
// No signature is specified in the settings, so use
// ~/.signature
// XXX This loading should be async, but that needs to
// be factored into how the signature HTML is passed
// to the editor.
File signature_file = File.new_for_path(Environment.get_home_dir()).get_child(".signature");
if (signature_file.query_exists()) {
try {
FileUtils.get_contents(signature_file.get_path(), out account_sig);
} catch (Error error) {
debug("Error reading signature file %s: %s", signature_file.get_path(), error.message);
}
}
}
account_sig = (!Geary.String.is_empty_or_whitespace(account_sig))
? Geary.HTML.smart_escape(account_sig, true)
: null;
}
this.signature_html = account_sig;
}
private void on_text_attributes_changed(uint mask) {
this.actions.change_action_state(
ACTION_BOLD,
@ -2335,14 +2250,4 @@ public class ComposerWidget : Gtk.EventBox {
link_dialog("http://");
}
// private static void on_link_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
// ComposerWidget composer) {
// try {
// composer.editor.get_dom_document().get_default_view().get_selection().
// select_all_children(element);
// } catch (Error e) {
// debug("Error selecting link: %s", e.message);
// }
// }
}

View file

@ -21,56 +21,6 @@ namespace Util.Composer {
private const string EDITING_DELETE_CONTAINER_ID = "WebKit-Editing-Delete-Container";
public void on_load_finished_and_realized(WebKit.WebPage page, string body_html) {
WebKit.DOM.Document document = page.get_dom_document();
WebKit.DOM.HTMLElement? body = document.get_element_by_id(BODY_ID) as WebKit.DOM.HTMLElement;
assert(body != null);
try {
body.set_inner_html(body_html);
} catch (Error e) {
debug("Failed to load prefilled body: %s", e.message);
}
protect_blockquote_styles(page);
// Focus within the HTML document
body.focus();
// Set cursor at appropriate position
try {
WebKit.DOM.Element? cursor = document.get_element_by_id("cursormarker");
if (cursor != null) {
WebKit.DOM.Range range = document.create_range();
range.select_node_contents(cursor);
range.collapse(false);
// WebKit.DOM.DOMSelection selection = document.default_page.get_selection();
// selection.remove_all_ranges();
// selection.add_range(range);
// cursor.parent_element.remove_child(cursor);
}
} catch (Error error) {
debug("Error setting cursor at end of text: %s", error.message);
}
//Util.DOM.bind_event(view, "a", "click", (Callback) on_link_clicked, this);
}
private void protect_blockquote_styles(WebKit.WebPage page) {
// We will search for an remove a particular styling when we quote text. If that style
// exists in the quoted text, we alter it slightly so we don't mess with it later.
try {
WebKit.DOM.NodeList node_list = page.get_dom_document().query_selector_all(
"blockquote[style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"]");
for (int i = 0; i < node_list.length; ++i) {
((WebKit.DOM.Element) node_list.item(i)).set_attribute("style",
"margin: 0 0 0 40px; padding: 0px; border:none;");
}
} catch (Error error) {
debug("Error protecting blockquotes: %s", error.message);
}
}
public void insert_quote(WebKit.WebPage page, string quote) {
WebKit.DOM.Document document = page.get_dom_document();
document.exec_command("insertHTML", false, quote);

View file

@ -10,6 +10,7 @@ set(RESOURCE_LIST
STRIPBLANKS "composer-headerbar.ui"
STRIPBLANKS "composer-menus.ui"
STRIPBLANKS "composer-widget.ui"
"composer-web-view.js"
STRIPBLANKS "conversation-email.ui"
STRIPBLANKS "conversation-email-attachment-view.ui"
STRIPBLANKS "conversation-email-menus.ui"

69
ui/composer-web-view.js Normal file
View file

@ -0,0 +1,69 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2016 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Application logic for ComposerWebView.
*/
var ComposerPageState = function() {
this.init.apply(this, arguments);
};
ComposerPageState.prototype = {
__proto__: PageState.prototype,
init: function() {
PageState.prototype.init.apply(this, []);
},
loaded: function() {
// Search for and remove a particular styling when we quote
// text. If that style exists in the quoted text, we alter it
// slightly so we don't mess with it later.
var nodeList = document.querySelectorAll(
"blockquote[style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"]");
for (var i = 0; i < nodeList.length; ++i) {
nodeList.item(i).setAttribute(
"style",
"margin: 0 0 0 40px; padding: 0px; border:none;"
);
}
// Focus within the HTML document
document.body.focus();
// Set cursor at appropriate position
var cursor = document.getElementById("cursormarker");
if (cursor != null) {
var range = document.createRange();
range.selectNodeContents(cursor);
range.collapse(false);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
cursor.parentNode.removeChild(cursor);
}
// Chain up here so we continue to a preferred size update
// after munging the HTML above.
PageState.prototype.loaded.apply(this, []);
//Util.DOM.bind_event(view, "a", "click", (Callback) on_link_clicked, this);
}
// private static void on_link_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
// ComposerWidget composer) {
// try {
// composer.editor.get_dom_document().get_default_view().get_selection().
// select_all_children(element);
// } catch (Error e) {
// debug("Error selecting link: %s", e.message);
// }
// }
};
var geary = new ComposerPageState();
window.onload = function() {
geary.loaded();
};