Merge branch 'mjog/558-webkit-shared-process-redux' into 'mainline'
Webkit shared process redux See merge request GNOME/geary!565
This commit is contained in:
commit
fac257a273
34 changed files with 1512 additions and 666 deletions
|
|
@ -56,7 +56,7 @@ valac = meson.get_compiler('vala')
|
|||
target_vala = '0.48.6'
|
||||
target_glib = '2.64'
|
||||
target_gtk = '3.24.7'
|
||||
target_webkit = '2.26'
|
||||
target_webkit = '2.28'
|
||||
|
||||
if not valac.version().version_compare('>=' + target_vala)
|
||||
error('Vala does not meet minimum required version: ' + target_vala)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ src/client/application/goa-mediator.vala
|
|||
src/client/application/main.vala
|
||||
src/client/application/secret-mediator.vala
|
||||
src/client/client-action.vala
|
||||
src/client/components/client-web-view.vala
|
||||
src/client/components/components-attachment-pane.vala
|
||||
src/client/components/components-conversation-actions.vala
|
||||
src/client/components/components-conversation-action-bar.vala
|
||||
|
|
@ -52,6 +51,7 @@ src/client/components/components-problem-report-info-bar.vala
|
|||
src/client/components/components-reflow-box.c
|
||||
src/client/components/components-search-bar.vala
|
||||
src/client/components/components-validator.vala
|
||||
src/client/components/components-web-view.vala
|
||||
src/client/components/count-badge.vala
|
||||
src/client/components/folder-popover.vala
|
||||
src/client/components/icon-factory.vala
|
||||
|
|
|
|||
|
|
@ -718,7 +718,7 @@ internal class Accounts.RemoveMailboxCommand : Application.Command {
|
|||
internal class Accounts.SignatureChangedCommand : Application.Command {
|
||||
|
||||
|
||||
private ClientWebView signature_view;
|
||||
private Components.WebView signature_view;
|
||||
private Geary.AccountInformation account;
|
||||
|
||||
private string old_value;
|
||||
|
|
@ -728,7 +728,7 @@ internal class Accounts.SignatureChangedCommand : Application.Command {
|
|||
private bool new_enabled = false;
|
||||
|
||||
|
||||
public SignatureChangedCommand(ClientWebView signature_view,
|
||||
public SignatureChangedCommand(Components.WebView signature_view,
|
||||
Geary.AccountInformation account) {
|
||||
this.signature_view = signature_view;
|
||||
this.account = account;
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
/**
|
||||
* A class for editing signatures in the accounts editor.
|
||||
*/
|
||||
public class Accounts.SignatureWebView : ClientWebView {
|
||||
public class Accounts.SignatureWebView : Components.WebView {
|
||||
|
||||
|
||||
private static WebKit.UserScript? app_script = null;
|
||||
|
||||
public static new void load_resources()
|
||||
throws GLib.Error {
|
||||
SignatureWebView.app_script = ClientWebView.load_app_script(
|
||||
SignatureWebView.app_script = Components.WebView.load_app_script(
|
||||
"signature-web-view.js"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,13 +134,12 @@ internal class Application.Controller :
|
|||
this.upgrade_dialog = new UpgradeDialog(application);
|
||||
|
||||
// Initialise WebKit and WebViews
|
||||
ClientWebView.init_web_context(
|
||||
Components.WebView.init_web_context(
|
||||
this.application.config,
|
||||
this.application.get_web_extensions_dir(),
|
||||
this.application.get_user_cache_directory().get_child("web-resources")
|
||||
);
|
||||
|
||||
ClientWebView.load_resources(
|
||||
Components.WebView.load_resources(
|
||||
this.application.get_user_config_directory()
|
||||
);
|
||||
Composer.WebView.load_resources();
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@
|
|||
*/
|
||||
|
||||
int main(string[] args) {
|
||||
// Temporary workaround for WebKitGTK deprecation of the
|
||||
// shared-secondary process model. Pull this out in 3.36 when the
|
||||
// proper fix lands. See GNOME/geary#558.
|
||||
Environment.set_variable("WEBKIT_USE_SINGLE_WEB_PROCESS", "1", true);
|
||||
|
||||
|
||||
// Init logging right up front so as to capture as many log
|
||||
// messages as possible
|
||||
Geary.Logging.init();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
* Copyright 2016 Michael Gratton <mike@vee.net>
|
||||
* Copyright © 2016 Software Freedom Conservancy Inc.
|
||||
* Copyright © 2016-2020 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.
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* integration, Inspector support, and remote and inline image
|
||||
* handling.
|
||||
*/
|
||||
public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
||||
public abstract class Components.WebView : WebKit.WebView, Geary.BaseInterface {
|
||||
|
||||
|
||||
/** URI Scheme and delimiter for internal resource loads. */
|
||||
|
|
@ -26,13 +26,17 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
/** URI Scheme and delimiter for images loaded by Content-ID. */
|
||||
public const string CID_URL_PREFIX = "cid:";
|
||||
|
||||
// Keep these in sync with GearyWebExtension
|
||||
private const string MESSAGE_RETURN_VALUE_NAME = "__return__";
|
||||
private const string MESSAGE_EXCEPTION_NAME = "__exception__";
|
||||
|
||||
// WebKit message handler names
|
||||
private const string COMMAND_STACK_CHANGED = "commandStackChanged";
|
||||
private const string CONTENT_LOADED = "contentLoaded";
|
||||
private const string DOCUMENT_MODIFIED = "documentModified";
|
||||
private const string PREFERRED_HEIGHT_CHANGED = "preferredHeightChanged";
|
||||
private const string REMOTE_IMAGE_LOAD_BLOCKED = "remoteImageLoadBlocked";
|
||||
private const string SELECTION_CHANGED = "selectionChanged";
|
||||
private const string COMMAND_STACK_CHANGED = "command_stack_changed";
|
||||
private const string CONTENT_LOADED = "content_loaded";
|
||||
private const string DOCUMENT_MODIFIED = "document_modified";
|
||||
private const string PREFERRED_HEIGHT_CHANGED = "preferred_height_changed";
|
||||
private const string REMOTE_IMAGE_LOAD_BLOCKED = "remote_image_load_blocked";
|
||||
private const string SELECTION_CHANGED = "selection_changed";
|
||||
|
||||
private const double ZOOM_DEFAULT = 1.0;
|
||||
private const double ZOOM_FACTOR = 0.1;
|
||||
|
|
@ -65,7 +69,6 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
private static WebKit.UserStyleSheet? user_stylesheet = null;
|
||||
|
||||
private static WebKit.UserScript? script = null;
|
||||
private static WebKit.UserScript? allow_remote_images = null;
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -76,23 +79,18 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
File cache_dir) {
|
||||
WebsiteDataManager data_manager = new WebsiteDataManager(cache_dir.get_path());
|
||||
WebKit.WebContext context = new WebKit.WebContext.with_website_data_manager(data_manager);
|
||||
#if HAS_WEBKIT_SHARED_PROC
|
||||
// Use a shared process so we don't spawn N WebProcess instances
|
||||
// when showing N messages in a conversation.
|
||||
context.set_process_model(WebKit.ProcessModel.SHARED_SECONDARY_PROCESS);
|
||||
#endif
|
||||
// Use the doc viewer model since each web view instance only
|
||||
// ever shows a single HTML document.
|
||||
context.set_cache_model(WebKit.CacheModel.DOCUMENT_VIEWER);
|
||||
|
||||
context.register_uri_scheme("cid", (req) => {
|
||||
ClientWebView? view = req.get_web_view() as ClientWebView;
|
||||
WebView? view = req.get_web_view() as WebView;
|
||||
if (view != null) {
|
||||
view.handle_cid_request(req);
|
||||
}
|
||||
});
|
||||
context.register_uri_scheme("geary", (req) => {
|
||||
ClientWebView? view = req.get_web_view() as ClientWebView;
|
||||
WebView? view = req.get_web_view() as WebView;
|
||||
if (view != null) {
|
||||
view.handle_internal_request(req);
|
||||
}
|
||||
|
|
@ -113,25 +111,22 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
update_spellcheck(context, config);
|
||||
});
|
||||
|
||||
ClientWebView.default_context = context;
|
||||
WebView.default_context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads static resources used by ClientWebView.
|
||||
* Loads static resources used by WebView.
|
||||
*/
|
||||
public static void load_resources(GLib.File user_dir)
|
||||
throws GLib.Error {
|
||||
ClientWebView.script = load_app_script(
|
||||
"client-web-view.js"
|
||||
);
|
||||
ClientWebView.allow_remote_images = load_app_script(
|
||||
"client-web-view-allow-remote-images.js"
|
||||
WebView.script = load_app_script(
|
||||
"components-web-view.js"
|
||||
);
|
||||
|
||||
foreach (string name in new string[] { USER_CSS, USER_CSS_LEGACY }) {
|
||||
GLib.File stylesheet = user_dir.get_child(name);
|
||||
try {
|
||||
ClientWebView.user_stylesheet = load_user_stylesheet(stylesheet);
|
||||
WebView.user_stylesheet = load_user_stylesheet(stylesheet);
|
||||
break;
|
||||
} catch (GLib.IOError.NOT_FOUND err) {
|
||||
// All good, try the next one or just exit
|
||||
|
|
@ -200,8 +195,23 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
|
||||
/** Delegate for UserContentManager message callbacks. */
|
||||
public delegate void JavaScriptMessageHandler(WebKit.JavascriptResult js_result);
|
||||
/**
|
||||
* Delegate for message handler callbacks.
|
||||
*
|
||||
* @see register_message_callback
|
||||
*/
|
||||
protected delegate void MessageCallback(GLib.Variant? parameters);
|
||||
|
||||
// Work around for not being able to put delegates in a Gee collection.
|
||||
private class MessageCallable {
|
||||
|
||||
public unowned MessageCallback handler;
|
||||
|
||||
public MessageCallable(MessageCallback handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the view's content has been fully loaded.
|
||||
|
|
@ -266,8 +276,8 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
private Gee.Map<string,Geary.Memory.Buffer> internal_resources =
|
||||
new Gee.HashMap<string,Geary.Memory.Buffer>();
|
||||
|
||||
private Gee.List<ulong> registered_message_handlers =
|
||||
new Gee.LinkedList<ulong>();
|
||||
private Gee.Map<string,MessageCallable> message_handlers =
|
||||
new Gee.HashMap<string,MessageCallable>();
|
||||
|
||||
private double webkit_reported_height = 0;
|
||||
|
||||
|
|
@ -299,8 +309,9 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
public signal void remote_image_load_blocked();
|
||||
|
||||
|
||||
protected ClientWebView(Application.Configuration config,
|
||||
WebKit.UserContentManager? custom_manager = null) {
|
||||
protected WebView(Application.Configuration config,
|
||||
WebKit.UserContentManager? custom_manager = null,
|
||||
WebView? related = null) {
|
||||
WebKit.Settings setts = new WebKit.Settings();
|
||||
setts.allow_modal_dialogs = false;
|
||||
setts.default_charset = "UTF-8";
|
||||
|
|
@ -321,70 +332,45 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
|
||||
WebKit.UserContentManager content_manager =
|
||||
custom_manager ?? new WebKit.UserContentManager();
|
||||
content_manager.add_script(ClientWebView.script);
|
||||
if (ClientWebView.user_stylesheet != null) {
|
||||
content_manager.add_style_sheet(ClientWebView.user_stylesheet);
|
||||
content_manager.add_script(WebView.script);
|
||||
if (WebView.user_stylesheet != null) {
|
||||
content_manager.add_style_sheet(WebView.user_stylesheet);
|
||||
}
|
||||
|
||||
Object(
|
||||
web_context: ClientWebView.default_context,
|
||||
settings: setts,
|
||||
user_content_manager: content_manager,
|
||||
settings: setts
|
||||
web_context: WebView.default_context
|
||||
);
|
||||
base_ref();
|
||||
|
||||
// XXX get the allow prefix from the extension somehow
|
||||
|
||||
this.decide_policy.connect(on_decide_policy);
|
||||
this.web_process_terminated.connect((reason) => {
|
||||
warning("Web process crashed: %s", reason.to_string());
|
||||
});
|
||||
|
||||
register_message_handler(
|
||||
COMMAND_STACK_CHANGED, on_command_stack_changed
|
||||
);
|
||||
register_message_handler(
|
||||
CONTENT_LOADED, on_content_loaded
|
||||
);
|
||||
register_message_handler(
|
||||
DOCUMENT_MODIFIED, on_document_modified
|
||||
);
|
||||
register_message_handler(
|
||||
PREFERRED_HEIGHT_CHANGED, on_preferred_height_changed
|
||||
);
|
||||
register_message_handler(
|
||||
REMOTE_IMAGE_LOAD_BLOCKED, on_remote_image_load_blocked
|
||||
);
|
||||
register_message_handler(
|
||||
SELECTION_CHANGED, on_selection_changed
|
||||
);
|
||||
|
||||
// Manage zoom level, ensure it's sane
|
||||
config.bind(Application.Configuration.CONVERSATION_VIEWER_ZOOM_KEY, this, "zoom_level");
|
||||
if (this.zoom_level < ZOOM_MIN) {
|
||||
this.zoom_level = ZOOM_MIN;
|
||||
} else if (this.zoom_level > ZOOM_MAX) {
|
||||
this.zoom_level = ZOOM_MAX;
|
||||
}
|
||||
this.scroll_event.connect(on_scroll_event);
|
||||
|
||||
// Watch desktop font settings
|
||||
Settings system_settings = config.gnome_interface;
|
||||
system_settings.bind("document-font-name", this,
|
||||
"document-font", SettingsBindFlags.DEFAULT);
|
||||
system_settings.bind("monospace-font-name", this,
|
||||
"monospace-font", SettingsBindFlags.DEFAULT);
|
||||
init(config);
|
||||
}
|
||||
|
||||
~ClientWebView() {
|
||||
/**
|
||||
* Constructs a new web view with a new shared WebProcess.
|
||||
*
|
||||
* The new view will use the same WebProcess, settings and content
|
||||
* manager as the given related view's.
|
||||
*
|
||||
* @see WebKit.WebView.WebView.with_related_view
|
||||
*/
|
||||
protected WebView.with_related_view(Application.Configuration config,
|
||||
WebView related) {
|
||||
Object(
|
||||
related_view: related,
|
||||
settings: related.get_settings(),
|
||||
user_content_manager: related.user_content_manager
|
||||
);
|
||||
base_ref();
|
||||
init(config);
|
||||
}
|
||||
|
||||
~WebView() {
|
||||
base_unref();
|
||||
}
|
||||
|
||||
public override void destroy() {
|
||||
foreach (ulong id in this.registered_message_handlers) {
|
||||
this.user_content_manager.disconnect(id);
|
||||
}
|
||||
this.registered_message_handlers.clear();
|
||||
this.message_handlers.clear();
|
||||
base.destroy();
|
||||
}
|
||||
|
||||
|
|
@ -400,9 +386,7 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
* Returns the view's content as an HTML string.
|
||||
*/
|
||||
public async string? get_html() throws Error {
|
||||
return Util.JS.to_string(
|
||||
yield call(Util.JS.callable("geary.getHtml"), null)
|
||||
);
|
||||
return yield call_returning<string?>(Util.JS.callable("getHtml"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -433,20 +417,14 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
* effect.
|
||||
*/
|
||||
public void allow_remote_image_loading() {
|
||||
// Use a separate script here since we need to update the
|
||||
// value of window.geary.allow_remote_image_loading after it
|
||||
// was first created by client-web-view.js (which is loaded at
|
||||
// the start of page load), but before the page load is
|
||||
// started (so that any remote images present are actually
|
||||
// loaded).
|
||||
this.user_content_manager.add_script(ClientWebView.allow_remote_images);
|
||||
this.run_javascript.begin("_gearyAllowRemoteResourceLoads = true", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load any remote images previously that were blocked.
|
||||
*/
|
||||
public void load_remote_images() {
|
||||
this.call.begin(Util.JS.callable("geary.loadRemoteImages"), null);
|
||||
this.call_void.begin(Util.JS.callable("loadRemoteImages"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -491,40 +469,192 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
public new async void set_editable(bool enabled,
|
||||
Cancellable? cancellable)
|
||||
throws Error {
|
||||
yield call(
|
||||
Util.JS.callable("geary.setEditable").bool(enabled), cancellable
|
||||
yield call_void(
|
||||
Util.JS.callable("setEditable").bool(enabled), cancellable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a {@link Util.JS.Callable} on this web view.
|
||||
*
|
||||
* This calls the given callable on the `geary` object for the
|
||||
* current view, any returned value are ignored.
|
||||
*/
|
||||
protected async JSC.Value call(Util.JS.Callable target,
|
||||
protected async void call_void(Util.JS.Callable target,
|
||||
GLib.Cancellable? cancellable)
|
||||
throws GLib.Error {
|
||||
WebKit.JavascriptResult result = yield run_javascript(
|
||||
target.to_string(), cancellable
|
||||
);
|
||||
return result.get_js_value();
|
||||
yield call_impl(target, cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for registering and connecting JS messages.
|
||||
* Invokes a {@link Util.JS.Callable} on this web view.
|
||||
*
|
||||
* This calls the given callable on the `geary` object for the
|
||||
* current view. The value returned by the call is returned by
|
||||
* this method.
|
||||
*
|
||||
* The type parameter `T` must match the type returned by the
|
||||
* call, else an error is thrown. Only simple nullable value types
|
||||
* are supported for T, for more complex return types (arrays,
|
||||
* dictionaries, etc) specify {@link GLib.Variant} for `T` and
|
||||
* manually parse that.
|
||||
*/
|
||||
protected inline void register_message_handler(string name,
|
||||
JavaScriptMessageHandler handler) {
|
||||
// XXX can't use the delegate directly, see b.g.o Bug
|
||||
// 604781. However the workaround below creates a circular
|
||||
// reference, causing ClientWebView instances to leak. So to
|
||||
// work around that we need to record handler ids and
|
||||
// disconnect them when being destroyed.
|
||||
ulong id = this.user_content_manager.script_message_received[name].connect(
|
||||
(result) => { handler(result); }
|
||||
protected async T call_returning<T>(Util.JS.Callable target,
|
||||
GLib.Cancellable? cancellable)
|
||||
throws GLib.Error {
|
||||
WebKit.UserMessage? response = yield call_impl(target, cancellable);
|
||||
if (response == null) {
|
||||
throw new Util.JS.Error.TYPE(
|
||||
"Method call %s did not return a value", target.to_string()
|
||||
);
|
||||
this.registered_message_handlers.add(id);
|
||||
if (!this.user_content_manager.register_script_message_handler(name)) {
|
||||
debug("Failed to register script message handler: %s", name);
|
||||
}
|
||||
GLib.Variant? param = response.parameters;
|
||||
T ret_value = null;
|
||||
var ret_type = typeof(T);
|
||||
if (ret_type == typeof(GLib.Variant)) {
|
||||
ret_value = param;
|
||||
} else {
|
||||
if (param != null && param.get_type().is_maybe()) {
|
||||
param = param.get_maybe();
|
||||
}
|
||||
if (param != null) {
|
||||
// Since these replies are coming from JS via
|
||||
// Util.JS.value_to_variant, they will only be one of
|
||||
// string, double, bool, array or dict
|
||||
var param_type = param.classify();
|
||||
if (ret_type == typeof(string) && param_type == STRING) {
|
||||
ret_value = param.get_string();
|
||||
} else if (ret_type == typeof(bool) && param_type == BOOLEAN) {
|
||||
ret_value = (bool?) param.get_boolean();
|
||||
} else if (ret_type == typeof(int) && param_type == DOUBLE) {
|
||||
ret_value = (int?) ((int) param.get_double());
|
||||
} else if (ret_type == typeof(short) && param_type == DOUBLE) {
|
||||
ret_value = (short?) ((short) param.get_double());
|
||||
} else if (ret_type == typeof(char) && param_type == DOUBLE) {
|
||||
ret_value = (char?) ((char) param.get_double());
|
||||
} else if (ret_type == typeof(long) && param_type == DOUBLE) {
|
||||
ret_value = (long?) ((long) param.get_double());
|
||||
} else if (ret_type == typeof(int64) && param_type == DOUBLE) {
|
||||
ret_value = (int64?) ((int64) param.get_double());
|
||||
} else if (ret_type == typeof(uint) && param_type == DOUBLE) {
|
||||
ret_value = (uint?) ((uint) param.get_double());
|
||||
} else if (ret_type == typeof(uchar) && param_type == DOUBLE) {
|
||||
ret_value = (uchar?) ((uchar) param.get_double());
|
||||
} else if (ret_type == typeof(ushort) && param_type == DOUBLE) {
|
||||
ret_value = (ushort?) ((ushort) param.get_double());
|
||||
} else if (ret_type == typeof(ulong) && param_type == DOUBLE) {
|
||||
ret_value = (ulong?) ((ulong) param.get_double());
|
||||
} else if (ret_type == typeof(uint64) && param_type == DOUBLE) {
|
||||
ret_value = (uint64?) ((uint64) param.get_double());
|
||||
} else if (ret_type == typeof(double) && param_type == DOUBLE) {
|
||||
ret_value = (double?) param.get_double();
|
||||
} else if (ret_type == typeof(float) && param_type == DOUBLE) {
|
||||
ret_value = (float?) ((float) param.get_double());
|
||||
} else {
|
||||
throw new Util.JS.Error.TYPE(
|
||||
"%s is not a supported type for %s",
|
||||
ret_type.name(), param_type.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback for a specific WebKit user message.
|
||||
*/
|
||||
protected void register_message_callback(string name,
|
||||
MessageCallback handler) {
|
||||
this.message_handlers.set(name, new MessageCallable(handler));
|
||||
}
|
||||
|
||||
private void init(Application.Configuration config) {
|
||||
// XXX get the allow prefix from the extension somehow
|
||||
|
||||
this.decide_policy.connect(on_decide_policy);
|
||||
this.web_process_terminated.connect((reason) => {
|
||||
warning("Web process crashed: %s", reason.to_string());
|
||||
});
|
||||
|
||||
register_message_callback(
|
||||
COMMAND_STACK_CHANGED, on_command_stack_changed
|
||||
);
|
||||
register_message_callback(
|
||||
CONTENT_LOADED, on_content_loaded
|
||||
);
|
||||
register_message_callback(
|
||||
DOCUMENT_MODIFIED, on_document_modified
|
||||
);
|
||||
register_message_callback(
|
||||
PREFERRED_HEIGHT_CHANGED, on_preferred_height_changed
|
||||
);
|
||||
register_message_callback(
|
||||
REMOTE_IMAGE_LOAD_BLOCKED, on_remote_image_load_blocked
|
||||
);
|
||||
register_message_callback(
|
||||
SELECTION_CHANGED, on_selection_changed
|
||||
);
|
||||
|
||||
this.user_message_received.connect(this.on_message_received);
|
||||
|
||||
// Manage zoom level, ensure it's sane
|
||||
config.bind(Application.Configuration.CONVERSATION_VIEWER_ZOOM_KEY, this, "zoom_level");
|
||||
if (this.zoom_level < ZOOM_MIN) {
|
||||
this.zoom_level = ZOOM_MIN;
|
||||
} else if (this.zoom_level > ZOOM_MAX) {
|
||||
this.zoom_level = ZOOM_MAX;
|
||||
}
|
||||
this.scroll_event.connect(on_scroll_event);
|
||||
|
||||
// Watch desktop font settings
|
||||
Settings system_settings = config.gnome_interface;
|
||||
system_settings.bind("document-font-name", this,
|
||||
"document-font", SettingsBindFlags.DEFAULT);
|
||||
system_settings.bind("monospace-font-name", this,
|
||||
"monospace-font", SettingsBindFlags.DEFAULT);
|
||||
}
|
||||
|
||||
private async WebKit.UserMessage? call_impl(Util.JS.Callable target,
|
||||
GLib.Cancellable? cancellable)
|
||||
throws GLib.Error {
|
||||
WebKit.UserMessage? response = yield send_message_to_page(
|
||||
target.to_message(), cancellable
|
||||
);
|
||||
if (response != null) {
|
||||
var response_name = response.name;
|
||||
if (response_name == MESSAGE_EXCEPTION_NAME) {
|
||||
var exception = new GLib.VariantDict(response.parameters);
|
||||
var name = exception.lookup_value("name", GLib.VariantType.STRING) as string;
|
||||
var message = exception.lookup_value("message", GLib.VariantType.STRING) as string;
|
||||
var backtrace = exception.lookup_value("backtrace_string", GLib.VariantType.STRING) as string;
|
||||
var source = exception.lookup_value("source_uri", GLib.VariantType.STRING) as string;
|
||||
var line = exception.lookup_value("line_number", GLib.VariantType.UINT32);
|
||||
var column = exception.lookup_value("column_number", GLib.VariantType.UINT32);
|
||||
|
||||
var log_message = "Method call %s raised %s exception at %s:%d:%d: %s".printf(
|
||||
target.to_string(),
|
||||
name ?? "unknown",
|
||||
source ?? "unknown",
|
||||
(line != null ? (int) line.get_uint32() : -1),
|
||||
(column != null ? (int) column.get_uint32() : -1),
|
||||
message ?? "unknown"
|
||||
);
|
||||
debug(log_message);
|
||||
if (backtrace != null) {
|
||||
debug(backtrace);
|
||||
}
|
||||
|
||||
throw new Util.JS.Error.EXCEPTION(log_message);
|
||||
} else if (response_name != MESSAGE_RETURN_VALUE_NAME) {
|
||||
throw new Util.JS.Error.TYPE(
|
||||
"Method call %s returned unknown name: %s",
|
||||
target.to_string(),
|
||||
response_name
|
||||
);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private void handle_cid_request(WebKit.URISchemeRequest request) {
|
||||
|
|
@ -625,12 +755,12 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void on_preferred_height_changed(WebKit.JavascriptResult result) {
|
||||
private void on_preferred_height_changed(GLib.Variant? parameters) {
|
||||
double height = this.webkit_reported_height;
|
||||
try {
|
||||
height = Util.JS.to_double(result.get_js_value());
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get preferred height: %s", err.message);
|
||||
if (parameters != null && parameters.classify() == DOUBLE) {
|
||||
height = parameters.get_double();
|
||||
} else {
|
||||
warning("Could not get JS preferred height");
|
||||
}
|
||||
|
||||
if (this.webkit_reported_height != height) {
|
||||
|
|
@ -639,41 +769,64 @@ public abstract class ClientWebView : WebKit.WebView, Geary.BaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
private void on_command_stack_changed(WebKit.JavascriptResult result) {
|
||||
try {
|
||||
string[] values =
|
||||
Util.JS.to_string(result.get_js_value()).split(",");
|
||||
command_stack_changed(values[0] == "true", values[1] == "true");
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get command stack state: %s", err.message);
|
||||
private void on_command_stack_changed(GLib.Variant? parameters) {
|
||||
if (parameters != null &&
|
||||
parameters.is_container() &&
|
||||
parameters.n_children() == 2) {
|
||||
GLib.Variant can_undo = parameters.get_child_value(0);
|
||||
GLib.Variant can_redo = parameters.get_child_value(1);
|
||||
command_stack_changed(
|
||||
can_undo.classify() == BOOLEAN && can_undo.get_boolean(),
|
||||
can_redo.classify() == BOOLEAN && can_redo.get_boolean()
|
||||
);
|
||||
} else {
|
||||
warning("Could not get JS command stack state");
|
||||
}
|
||||
}
|
||||
|
||||
private void on_document_modified(WebKit.JavascriptResult result) {
|
||||
private void on_document_modified(GLib.Variant? parameters) {
|
||||
document_modified();
|
||||
}
|
||||
|
||||
private void on_remote_image_load_blocked(WebKit.JavascriptResult result) {
|
||||
private void on_remote_image_load_blocked(GLib.Variant? parameters) {
|
||||
remote_image_load_blocked();
|
||||
}
|
||||
|
||||
private void on_content_loaded(WebKit.JavascriptResult result) {
|
||||
private void on_content_loaded(GLib.Variant? parameters) {
|
||||
this.is_content_loaded = true;
|
||||
content_loaded();
|
||||
}
|
||||
|
||||
private void on_selection_changed(WebKit.JavascriptResult result) {
|
||||
try {
|
||||
bool has_selection = Util.JS.to_bool(result.get_js_value());
|
||||
// Avoid firing multiple notifies if the value hasn't
|
||||
// changed
|
||||
if (this.has_selection != has_selection) {
|
||||
this.has_selection = has_selection;
|
||||
private void on_selection_changed(GLib.Variant? parameters) {
|
||||
if (parameters != null && parameters.classify() == BOOLEAN) {
|
||||
selection_changed(parameters.get_boolean());
|
||||
} else {
|
||||
warning("Could not get JS selection value");
|
||||
}
|
||||
selection_changed(has_selection);
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get selection content: %s", err.message);
|
||||
}
|
||||
|
||||
private bool on_message_received(WebKit.UserMessage message) {
|
||||
if (message.name == MESSAGE_EXCEPTION_NAME) {
|
||||
var detail = new GLib.VariantDict(message.parameters);
|
||||
var name = detail.lookup_value("name", GLib.VariantType.STRING) as string;
|
||||
var log_message = detail.lookup_value("message", GLib.VariantType.STRING) as string;
|
||||
warning(
|
||||
"Error sending message from JS: %s: %s",
|
||||
name ?? "unknown",
|
||||
log_message ?? "unknown"
|
||||
);
|
||||
} else if (this.message_handlers.has_key(message.name)) {
|
||||
debug(
|
||||
"Message received: %s(%s)",
|
||||
message.name,
|
||||
message.parameters != null ? message.parameters.print(true) : ""
|
||||
);
|
||||
MessageCallable callback = this.message_handlers.get(message.name);
|
||||
callback.handler(message.parameters);
|
||||
} else {
|
||||
warning("Message with unknown handler received: %s", message.name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
/**
|
||||
* A WebView for editing messages in the composer.
|
||||
*/
|
||||
public class Composer.WebView : ClientWebView {
|
||||
public class Composer.WebView : Components.WebView {
|
||||
|
||||
/** HTML id used for the main text section of the message body. */
|
||||
public const string BODY_HTML_ID = "geary-body";
|
||||
|
|
@ -33,8 +33,8 @@ public class Composer.WebView : ClientWebView {
|
|||
private const string SPACER = "<div><br /></div>";
|
||||
|
||||
// WebKit message handler names
|
||||
private const string CURSOR_CONTEXT_CHANGED = "cursorContextChanged";
|
||||
private const string DRAG_DROP_RECEIVED = "dragDropReceived";
|
||||
private const string CURSOR_CONTEXT_CHANGED = "cursor_context_changed";
|
||||
private const string DRAG_DROP_RECEIVED = "drag_drop_received";
|
||||
|
||||
/**
|
||||
* Encapsulates editing-related state for a specific DOM node.
|
||||
|
|
@ -112,10 +112,10 @@ public class Composer.WebView : ClientWebView {
|
|||
|
||||
public static new void load_resources()
|
||||
throws Error {
|
||||
WebView.app_style = ClientWebView.load_app_stylesheet(
|
||||
WebView.app_style = Components.WebView.load_app_stylesheet(
|
||||
"composer-web-view.css"
|
||||
);
|
||||
WebView.app_script = ClientWebView.load_app_script(
|
||||
WebView.app_script = Components.WebView.load_app_script(
|
||||
"composer-web-view.js"
|
||||
);
|
||||
}
|
||||
|
|
@ -152,8 +152,8 @@ public class Composer.WebView : ClientWebView {
|
|||
this.user_content_manager.add_style_sheet(WebView.app_style);
|
||||
this.user_content_manager.add_script(WebView.app_script);
|
||||
|
||||
register_message_handler(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
|
||||
register_message_handler(DRAG_DROP_RECEIVED, on_drag_drop_received);
|
||||
register_message_callback(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
|
||||
register_message_callback(DRAG_DROP_RECEIVED, on_drag_drop_received);
|
||||
|
||||
// XXX this is a bit of a hack given the docs for is_empty,
|
||||
// above
|
||||
|
|
@ -202,8 +202,8 @@ public class Composer.WebView : ClientWebView {
|
|||
* Returns the view's content as HTML without being cleaned.
|
||||
*/
|
||||
public async string? get_html_for_draft() throws Error {
|
||||
return Util.JS.to_string(
|
||||
yield call(Util.JS.callable("geary.getHtml").bool(false), null)
|
||||
return yield call_returning<string?>(
|
||||
Util.JS.callable("getHtml").bool(false), null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -213,8 +213,8 @@ public class Composer.WebView : ClientWebView {
|
|||
public void set_rich_text(bool enabled) {
|
||||
this.is_rich_text = enabled;
|
||||
if (this.is_content_loaded) {
|
||||
this.call.begin(
|
||||
Util.JS.callable("geary.setRichText").bool(enabled), null
|
||||
this.call_void.begin(
|
||||
Util.JS.callable("setRichText").bool(enabled), null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -223,14 +223,14 @@ public class Composer.WebView : ClientWebView {
|
|||
* Undoes the last edit operation.
|
||||
*/
|
||||
public void undo() {
|
||||
this.call.begin(Util.JS.callable("geary.undo"), null);
|
||||
this.call_void.begin(Util.JS.callable("undo"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the last undone edit operation.
|
||||
*/
|
||||
public void redo() {
|
||||
this.call.begin(Util.JS.callable("geary.redo"), null);
|
||||
this.call_void.begin(Util.JS.callable("redo"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -239,9 +239,9 @@ public class Composer.WebView : ClientWebView {
|
|||
* Returns an id to be used to refer to the selection in
|
||||
* subsequent calls.
|
||||
*/
|
||||
public async string save_selection() throws Error {
|
||||
return Util.JS.to_string(
|
||||
yield call(Util.JS.callable("geary.saveSelection"), null)
|
||||
public async string? save_selection() throws Error {
|
||||
return yield call_returning<string?>(
|
||||
Util.JS.callable("saveSelection"), null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -249,9 +249,7 @@ public class Composer.WebView : ClientWebView {
|
|||
* Removes a saved selection.
|
||||
*/
|
||||
public void free_selection(string id) {
|
||||
this.call.begin(
|
||||
Util.JS.callable("geary.freeSelection").string(id), null
|
||||
);
|
||||
this.call_void.begin(Util.JS.callable("freeSelection").string(id), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -357,9 +355,9 @@ public class Composer.WebView : ClientWebView {
|
|||
* will be inserted wrapping the selection.
|
||||
*/
|
||||
public void insert_link(string href, string selection_id) {
|
||||
this.call.begin(
|
||||
this.call_void.begin(
|
||||
Util.JS.callable(
|
||||
"geary.insertLink"
|
||||
"insertLink"
|
||||
).string(href).string(selection_id),
|
||||
null
|
||||
);
|
||||
|
|
@ -373,8 +371,8 @@ public class Composer.WebView : ClientWebView {
|
|||
* unlinked section.
|
||||
*/
|
||||
public void delete_link(string selection_id) {
|
||||
this.call.begin(
|
||||
Util.JS.callable("geary.deleteLink").string(selection_id),
|
||||
this.call_void.begin(
|
||||
Util.JS.callable("deleteLink").string(selection_id),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
|
@ -396,23 +394,23 @@ public class Composer.WebView : ClientWebView {
|
|||
* Indents the line at the current text cursor location.
|
||||
*/
|
||||
public void indent_line() {
|
||||
this.call.begin(Util.JS.callable("geary.indentLine"), null);
|
||||
this.call_void.begin(Util.JS.callable("indentLine"), null);
|
||||
}
|
||||
|
||||
public void insert_olist() {
|
||||
this.call.begin(Util.JS.callable("geary.insertOrderedList"), null);
|
||||
this.call_void.begin(Util.JS.callable("insertOrderedList"), null);
|
||||
}
|
||||
|
||||
public void insert_ulist() {
|
||||
this.call.begin(Util.JS.callable("geary.insertUnorderedList"), null);
|
||||
this.call_void.begin(Util.JS.callable("insertUnorderedList"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the signature block if it has not been deleted.
|
||||
*/
|
||||
public new void update_signature(string signature) {
|
||||
this.call.begin(
|
||||
Util.JS.callable("geary.updateSignature").string(signature), null
|
||||
this.call_void.begin(
|
||||
Util.JS.callable("updateSignature").string(signature), null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -420,21 +418,20 @@ public class Composer.WebView : ClientWebView {
|
|||
* Removes the quoted message (if any) from the composer.
|
||||
*/
|
||||
public void delete_quoted_message() {
|
||||
this.call.begin(Util.JS.callable("geary.deleteQuotedMessage"), null);
|
||||
this.call_void.begin(Util.JS.callable("deleteQuotedMessage"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the editor content contains an attachment keyword.
|
||||
*/
|
||||
public async bool contains_attachment_keywords(string keyword_spec,
|
||||
public async bool? contains_attachment_keywords(string keyword_spec,
|
||||
string subject) {
|
||||
try {
|
||||
return Util.JS.to_bool(
|
||||
yield call(
|
||||
Util.JS.callable("geary.containsAttachmentKeyword")
|
||||
return yield call_returning<bool?>(
|
||||
Util.JS.callable("containsAttachmentKeyword")
|
||||
.string(keyword_spec)
|
||||
.string(subject),
|
||||
null)
|
||||
null
|
||||
);
|
||||
} catch (Error err) {
|
||||
debug("Error checking or attachment keywords: %s", err.message);
|
||||
|
|
@ -449,7 +446,7 @@ public class Composer.WebView : ClientWebView {
|
|||
* this.
|
||||
*/
|
||||
public async void clean_content() throws Error {
|
||||
this.call.begin(Util.JS.callable("geary.cleanContent"), null);
|
||||
this.call_void.begin(Util.JS.callable("cleanContent"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -459,10 +456,10 @@ public class Composer.WebView : ClientWebView {
|
|||
const int MAX_BREAKABLE_LEN = 72; // F=F recommended line limit
|
||||
const int MAX_UNBREAKABLE_LEN = 998; // SMTP line limit
|
||||
|
||||
string body_text = Util.JS.to_string(
|
||||
yield call(Util.JS.callable("geary.getText"), null)
|
||||
string? body_text = yield call_returning<string?>(
|
||||
Util.JS.callable("getText"), null
|
||||
);
|
||||
string[] lines = body_text.split("\n");
|
||||
string[] lines = (body_text ?? "").split("\n");
|
||||
GLib.StringBuilder flowed = new GLib.StringBuilder.sized(body_text.length);
|
||||
foreach (string line in lines) {
|
||||
// Strip trailing whitespace, so it doesn't look like a
|
||||
|
|
@ -533,39 +530,35 @@ public class Composer.WebView : ClientWebView {
|
|||
return ret;
|
||||
}
|
||||
|
||||
private void on_cursor_context_changed(WebKit.JavascriptResult result) {
|
||||
try {
|
||||
cursor_context_changed(
|
||||
new EditContext(Util.JS.to_string(result.get_js_value()))
|
||||
);
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get text cursor style: %s", err.message);
|
||||
private void on_cursor_context_changed(GLib.Variant? parameters) {
|
||||
if (parameters != null && parameters.classify() == STRING) {
|
||||
cursor_context_changed(new EditContext(parameters as string));
|
||||
} else {
|
||||
warning("Could not get text cursor style");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a dropped image
|
||||
*/
|
||||
private void on_drag_drop_received(WebKit.JavascriptResult result) {
|
||||
private void on_drag_drop_received(GLib.Variant? parameters) {
|
||||
var dict = new GLib.VariantDict(parameters);
|
||||
string file_name = dict.lookup_value(
|
||||
"fileName", GLib.VariantType.STRING
|
||||
).get_string();
|
||||
string file_name_unescaped = GLib.Uri.unescape_string(file_name);
|
||||
|
||||
try {
|
||||
JSC.Value object = result.get_js_value();
|
||||
string filename = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "fileName")
|
||||
);
|
||||
string filename_unescaped = GLib.Uri.unescape_string(filename);
|
||||
string file_type = dict.lookup_value(
|
||||
"fileType", GLib.VariantType.STRING
|
||||
).get_string();
|
||||
|
||||
string file_type = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "fileType")
|
||||
);
|
||||
|
||||
string content_base64 = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "content")
|
||||
);
|
||||
string content_base64 = dict.lookup_value(
|
||||
"content", GLib.VariantType.STRING
|
||||
).get_string();
|
||||
uint8[] image = GLib.Base64.decode(content_base64);
|
||||
|
||||
if (image.length == 0) {
|
||||
warning("%s is empty", filename);
|
||||
warning("%s is empty", file_name);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -573,10 +566,7 @@ public class Composer.WebView : ClientWebView {
|
|||
// will be this accepting types which won't be supported by WebKit
|
||||
// or recipients.
|
||||
if (file_type.index_of("image/") == 0) {
|
||||
image_file_dropped(filename_unescaped, file_type, image);
|
||||
}
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get deceptive link param: %s", err.message);
|
||||
image_file_dropped(file_name_unescaped, file_type, image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1252,7 +1252,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
|
|||
email.inline_files.set_all(this.inline_files);
|
||||
email.cid_files.set_all(this.cid_files);
|
||||
|
||||
email.img_src_prefix = ClientWebView.INTERNAL_URL_PREFIX;
|
||||
email.img_src_prefix = Components.WebView.INTERNAL_URL_PREFIX;
|
||||
|
||||
try {
|
||||
email.body_text = yield this.editor.body.get_text();
|
||||
|
|
@ -1450,16 +1450,17 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
|
|||
confirmation = _("Send message with an empty subject?");
|
||||
} else if (!has_body && !has_attachment) {
|
||||
confirmation = _("Send message with an empty body?");
|
||||
} else if (!has_attachment &&
|
||||
yield this.editor.body.contains_attachment_keywords(
|
||||
string.join(
|
||||
"|",
|
||||
ATTACHMENT_KEYWORDS,
|
||||
ATTACHMENT_KEYWORDS_LOCALISED
|
||||
),
|
||||
this.subject)) {
|
||||
} else if (!has_attachment) {
|
||||
var keywords = string.join(
|
||||
"|", ATTACHMENT_KEYWORDS, ATTACHMENT_KEYWORDS_LOCALISED
|
||||
);
|
||||
var contains = yield this.editor.body.contains_attachment_keywords(
|
||||
keywords, this.subject
|
||||
);
|
||||
if (contains != null && contains) {
|
||||
confirmation = _("Send message without an attachment?");
|
||||
}
|
||||
}
|
||||
if (confirmation != null) {
|
||||
ConfirmationDialog dialog = new ConfirmationDialog(container.top_window,
|
||||
confirmation, null, Stock._OK, "suggested-action");
|
||||
|
|
@ -1924,7 +1925,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
|
|||
string unique_filename;
|
||||
add_inline_part(byte_buffer, filename, out unique_filename);
|
||||
this.editor.body.insert_image(
|
||||
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
Components.WebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
);
|
||||
} catch (Error error) {
|
||||
this.application.report_problem(
|
||||
|
|
@ -1964,7 +1965,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
|
|||
string unique_filename;
|
||||
add_inline_part(file_buffer, path, out unique_filename);
|
||||
this.editor.body.insert_image(
|
||||
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
Components.WebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
);
|
||||
} catch (Error err) {
|
||||
attachment_failed(err.message);
|
||||
|
|
@ -2459,7 +2460,7 @@ public class Composer.Widget : Gtk.EventBox, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
this.editor.body.insert_image(
|
||||
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
Components.WebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
if (this.body_selection_message != null) {
|
||||
try {
|
||||
selection =
|
||||
yield this.body_selection_message.web_view.get_selection_for_quoting();
|
||||
yield this.body_selection_message.get_selection_for_quoting();
|
||||
} catch (Error err) {
|
||||
debug("Failed to get selection for quoting: %s", err.message);
|
||||
}
|
||||
|
|
@ -478,7 +478,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
if (this.body_selection_message != null) {
|
||||
try {
|
||||
selection =
|
||||
yield this.body_selection_message.web_view.get_selection_for_find();
|
||||
yield this.body_selection_message.get_selection_for_find();
|
||||
} catch (Error err) {
|
||||
debug("Failed to get selection for find: %s", err.message);
|
||||
}
|
||||
|
|
@ -571,12 +571,10 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
Json.Generator generator = new Json.Generator();
|
||||
generator.set_root(builder.get_root());
|
||||
string js = "geary.addPrintHeaders(" + generator.to_data(null) + ");";
|
||||
yield this.primary_message.web_view.run_javascript(js, null);
|
||||
yield this.primary_message.run_javascript(js, null);
|
||||
|
||||
Gtk.Window? window = get_toplevel() as Gtk.Window;
|
||||
WebKit.PrintOperation op = new WebKit.PrintOperation(
|
||||
this.primary_message.web_view
|
||||
);
|
||||
WebKit.PrintOperation op = this.primary_message.new_print_operation();
|
||||
Gtk.PrintSettings settings = new Gtk.PrintSettings();
|
||||
|
||||
if (this.email.subject != null) {
|
||||
|
|
@ -603,14 +601,14 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
private void connect_message_view_signals(ConversationMessage view) {
|
||||
view.content_loaded.connect(on_content_loaded);
|
||||
view.flag_remote_images.connect(on_flag_remote_images);
|
||||
view.internal_link_activated.connect((y) => {
|
||||
internal_link_activated(y);
|
||||
});
|
||||
view.internal_resource_loaded.connect(on_resource_loaded);
|
||||
view.save_image.connect(on_save_image);
|
||||
view.web_view.internal_resource_loaded.connect(on_resource_loaded);
|
||||
view.web_view.content_loaded.connect(on_content_loaded);
|
||||
view.web_view.selection_changed.connect((has_selection) => {
|
||||
view.selection_changed.connect((has_selection) => {
|
||||
this.body_selection_message = has_selection ? view : null;
|
||||
body_selection_changed(has_selection);
|
||||
});
|
||||
|
|
@ -686,7 +684,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
|
||||
// Load all messages
|
||||
|
||||
this.primary_message.web_view.add_internal_resources(cid_resources);
|
||||
this.primary_message.add_internal_resources(cid_resources);
|
||||
yield this.primary_message.load_message_body(
|
||||
message, this.load_cancellable
|
||||
);
|
||||
|
|
@ -704,7 +702,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
this.config
|
||||
);
|
||||
connect_message_view_signals(attached_message);
|
||||
attached_message.web_view.add_internal_resources(cid_resources);
|
||||
attached_message.add_internal_resources(cid_resources);
|
||||
this.sub_messages.add(attached_message);
|
||||
this._attached_messages.add(attached_message);
|
||||
attached_message.load_contacts.begin(this.load_cancellable);
|
||||
|
|
@ -886,8 +884,8 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
Geary.Memory.Buffer? content) {
|
||||
var main = get_toplevel() as Application.MainWindow;
|
||||
if (main != null) {
|
||||
if (uri.has_prefix(ClientWebView.CID_URL_PREFIX)) {
|
||||
string cid = uri.substring(ClientWebView.CID_URL_PREFIX.length);
|
||||
if (uri.has_prefix(Components.WebView.CID_URL_PREFIX)) {
|
||||
string cid = uri.substring(Components.WebView.CID_URL_PREFIX.length);
|
||||
try {
|
||||
Geary.Attachment attachment = this.email.get_attachment_by_content_id(
|
||||
cid
|
||||
|
|
@ -934,7 +932,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
|
|||
private void on_content_loaded() {
|
||||
bool all_loaded = true;
|
||||
foreach (ConversationMessage message in this) {
|
||||
if (!message.web_view.is_content_loaded) {
|
||||
if (!message.is_content_loaded) {
|
||||
all_loaded = false;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -957,7 +957,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
*/
|
||||
public void zoom_in() {
|
||||
message_view_iterator().foreach((msg_view) => {
|
||||
msg_view.web_view.zoom_in();
|
||||
msg_view.zoom_in();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
@ -967,7 +967,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
*/
|
||||
public void zoom_out() {
|
||||
message_view_iterator().foreach((msg_view) => {
|
||||
msg_view.web_view.zoom_out();
|
||||
msg_view.zoom_out();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
@ -977,7 +977,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
*/
|
||||
public void zoom_reset() {
|
||||
message_view_iterator().foreach((msg_view) => {
|
||||
msg_view.web_view.zoom_reset();
|
||||
msg_view.zoom_reset();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
@ -1182,8 +1182,7 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
row.get_allocation(out alloc);
|
||||
|
||||
int x = 0, y = 0;
|
||||
ConversationWebView web_view = row.view.primary_message.web_view;
|
||||
web_view.translate_coordinates(row, x, anchor_y, out x, out y);
|
||||
row.view.primary_message.web_view_translate_coordinates(row, x, anchor_y, out x, out y);
|
||||
|
||||
Gtk.Adjustment adj = get_adjustment();
|
||||
y = alloc.y + y;
|
||||
|
|
@ -1216,14 +1215,13 @@ public class ConversationListBox : Gtk.ListBox, Geary.BaseInterface {
|
|||
ConversationMessage conversation_message = view.primary_message;
|
||||
int body_top = 0;
|
||||
int body_left = 0;
|
||||
ConversationWebView web_view = conversation_message.web_view;
|
||||
web_view.translate_coordinates(
|
||||
conversation_message.web_view_translate_coordinates(
|
||||
this,
|
||||
0, 0,
|
||||
out body_left, out body_top
|
||||
);
|
||||
|
||||
int body_height = web_view.get_allocated_height();
|
||||
int body_height = conversation_message.web_view_get_allocated_height();
|
||||
int body_bottom = body_top + body_height;
|
||||
|
||||
// Only mark the email as read if it's actually visible
|
||||
|
|
|
|||
|
|
@ -310,8 +310,19 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
[GtkChild]
|
||||
internal Components.InfoBarStack info_bars;
|
||||
|
||||
/**
|
||||
* Emitted when web_view's content has finished loaded.
|
||||
*
|
||||
* See {@link Components.WebView.is_content_loaded} for details.
|
||||
*/
|
||||
internal bool is_content_loaded {
|
||||
get {
|
||||
return this.web_view != null && this.web_view.is_content_loaded;
|
||||
}
|
||||
}
|
||||
|
||||
/** HTML view that displays the message body. */
|
||||
internal ConversationWebView web_view { get; private set; }
|
||||
private ConversationWebView? web_view { get; private set; }
|
||||
|
||||
// The message headers represented by this view
|
||||
private Geary.EmailHeaderSet headers;
|
||||
|
|
@ -426,6 +437,19 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
string uri, string? alt_text, Geary.Memory.Buffer? buffer
|
||||
);
|
||||
|
||||
/** Emitted when web_view has loaded a resource added to it. */
|
||||
public signal void internal_resource_loaded(string name);
|
||||
|
||||
/** Emitted when web_view's selection has changed. */
|
||||
public signal void selection_changed(bool has_selection);
|
||||
|
||||
/**
|
||||
* Emitted when web_view's content has finished loaded.
|
||||
*
|
||||
* See {@link Components.WebView.is_content_loaded} for details.
|
||||
*/
|
||||
public signal void content_loaded();
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new view from an email's headers and body.
|
||||
|
|
@ -467,6 +491,18 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
);
|
||||
}
|
||||
|
||||
private void trigger_internal_resource_loaded(string name) {
|
||||
internal_resource_loaded(name);
|
||||
}
|
||||
|
||||
private void trigger_content_loaded() {
|
||||
content_loaded();
|
||||
}
|
||||
|
||||
private void trigger_selection_changed(bool has_selection) {
|
||||
selection_changed(has_selection);
|
||||
}
|
||||
|
||||
private ConversationMessage(Geary.EmailHeaderSet headers,
|
||||
string? preview,
|
||||
bool load_remote_resources,
|
||||
|
|
@ -487,19 +523,10 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
.activate.connect(on_copy_email_address);
|
||||
add_action(ACTION_COPY_LINK, true, VariantType.STRING)
|
||||
.activate.connect(on_copy_link);
|
||||
add_action(ACTION_COPY_SELECTION, false).activate.connect(() => {
|
||||
web_view.copy_clipboard();
|
||||
});
|
||||
add_action(ACTION_OPEN_INSPECTOR, config.enable_inspector).activate.connect(() => {
|
||||
this.web_view.get_inspector().show();
|
||||
});
|
||||
add_action(ACTION_OPEN_LINK, true, VariantType.STRING)
|
||||
.activate.connect(on_link_activated);
|
||||
add_action(ACTION_SAVE_IMAGE, true, new VariantType("(sms)"))
|
||||
.activate.connect(on_save_image);
|
||||
add_action(ACTION_SELECT_ALL, true).activate.connect(() => {
|
||||
web_view.select_all();
|
||||
});
|
||||
insert_action_group("msg", message_actions);
|
||||
|
||||
// Context menu
|
||||
|
|
@ -552,25 +579,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
this.subject_searchable = headers.subject.value.casefold();
|
||||
}
|
||||
|
||||
// Web view
|
||||
|
||||
this.web_view = new ConversationWebView(config);
|
||||
this.web_view.context_menu.connect(on_context_menu);
|
||||
this.web_view.deceptive_link_clicked.connect(on_deceptive_link_clicked);
|
||||
this.web_view.link_activated.connect((link) => {
|
||||
on_link_activated(new GLib.Variant("s", link));
|
||||
});
|
||||
this.web_view.mouse_target_changed.connect(on_mouse_target_changed);
|
||||
this.web_view.notify["is-loading"].connect(on_is_loading_notify);
|
||||
this.web_view.resource_load_started.connect(on_resource_load_started);
|
||||
this.web_view.remote_image_load_blocked.connect(on_remote_images_blocked);
|
||||
this.web_view.selection_changed.connect(on_selection_changed);
|
||||
this.web_view.set_hexpand(true);
|
||||
this.web_view.set_vexpand(true);
|
||||
this.web_view.show();
|
||||
|
||||
this.body_container.set_has_tooltip(true); // Used to show link URLs
|
||||
this.body_container.add(this.web_view);
|
||||
this.show_progress_timeout = new Geary.TimeoutManager.milliseconds(
|
||||
Util.Gtk.SHOW_PROGRESS_TIMEOUT_MSEC, this.on_show_progress_timeout
|
||||
);
|
||||
|
|
@ -584,6 +593,51 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
this.progress_pulse.repetition = FOREVER;
|
||||
}
|
||||
|
||||
private void initialize_web_view() {
|
||||
var viewer = get_ancestor(typeof(ConversationViewer)) as ConversationViewer;
|
||||
|
||||
// Ensure we share the same WebProcess with the last one
|
||||
// constructed if possible.
|
||||
if (viewer != null && viewer.previous_web_view != null) {
|
||||
this.web_view = new ConversationWebView.with_related_view(
|
||||
this.config,
|
||||
viewer.previous_web_view
|
||||
);
|
||||
} else {
|
||||
this.web_view = new ConversationWebView(this.config);
|
||||
}
|
||||
if (viewer != null) {
|
||||
viewer.previous_web_view = this.web_view;
|
||||
}
|
||||
|
||||
this.web_view.context_menu.connect(on_context_menu);
|
||||
this.web_view.deceptive_link_clicked.connect(on_deceptive_link_clicked);
|
||||
this.web_view.link_activated.connect((link) => {
|
||||
on_link_activated(new GLib.Variant("s", link));
|
||||
});
|
||||
this.web_view.mouse_target_changed.connect(on_mouse_target_changed);
|
||||
this.web_view.notify["is-loading"].connect(on_is_loading_notify);
|
||||
this.web_view.resource_load_started.connect(on_resource_load_started);
|
||||
this.web_view.remote_image_load_blocked.connect(on_remote_images_blocked);
|
||||
this.web_view.selection_changed.connect(on_selection_changed);
|
||||
this.web_view.internal_resource_loaded.connect(trigger_internal_resource_loaded);
|
||||
this.web_view.content_loaded.connect(trigger_content_loaded);
|
||||
this.web_view.selection_changed.connect(trigger_selection_changed);
|
||||
this.web_view.set_hexpand(true);
|
||||
this.web_view.set_vexpand(true);
|
||||
this.web_view.show();
|
||||
this.body_container.add(this.web_view);
|
||||
add_action(ACTION_COPY_SELECTION, false).activate.connect(() => {
|
||||
web_view.copy_clipboard();
|
||||
});
|
||||
add_action(ACTION_OPEN_INSPECTOR, config.enable_inspector).activate.connect(() => {
|
||||
this.web_view.get_inspector().show();
|
||||
});
|
||||
add_action(ACTION_SELECT_ALL, true).activate.connect(() => {
|
||||
web_view.select_all();
|
||||
});
|
||||
}
|
||||
|
||||
~ConversationMessage() {
|
||||
base_unref();
|
||||
}
|
||||
|
|
@ -597,10 +651,77 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
base.destroy();
|
||||
}
|
||||
|
||||
public async string? get_selection_for_quoting() throws Error {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
return yield web_view.get_selection_for_quoting();
|
||||
}
|
||||
|
||||
public async string? get_selection_for_find() throws Error {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
return yield web_view.get_selection_for_find();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of internal resources to web_view.
|
||||
*
|
||||
* @see Components.WebView.add_internal_resources
|
||||
*/
|
||||
public void add_internal_resources(Gee.Map<string,Geary.Memory.Buffer> res) {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
web_view.add_internal_resources(res);
|
||||
}
|
||||
|
||||
public WebKit.PrintOperation new_print_operation() {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
return new WebKit.PrintOperation(web_view);
|
||||
}
|
||||
|
||||
public async void run_javascript (string script, Cancellable? cancellable) throws Error {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
yield web_view.run_javascript(script, cancellable);
|
||||
}
|
||||
|
||||
public void zoom_in() {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
web_view.zoom_in();
|
||||
}
|
||||
|
||||
public void zoom_out() {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
web_view.zoom_out();
|
||||
}
|
||||
|
||||
public void zoom_reset() {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
web_view.zoom_reset();
|
||||
}
|
||||
|
||||
public void web_view_translate_coordinates(Gtk.Widget widget, int x, int anchor_y, out int x1, out int y1) {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
web_view.translate_coordinates(widget, x, anchor_y, out x1, out y1);
|
||||
}
|
||||
|
||||
public int web_view_get_allocated_height() {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
return web_view.get_allocated_height();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the complete message and hides the compact headers.
|
||||
*/
|
||||
public void show_message_body(bool include_transitions=true) {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
set_revealer(this.compact_revealer, false, include_transitions);
|
||||
set_revealer(this.header_revealer, true, include_transitions);
|
||||
set_revealer(this.body_revealer, true, include_transitions);
|
||||
|
|
@ -785,6 +906,10 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
throw new GLib.IOError.CANCELLED("Conversation load cancelled");
|
||||
}
|
||||
|
||||
if (this.web_view == null) {
|
||||
initialize_web_view();
|
||||
}
|
||||
|
||||
bool contact_load_images = (
|
||||
this.primary_contact != null &&
|
||||
this.primary_contact.load_remote_resources
|
||||
|
|
@ -835,6 +960,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
uint webkit_found = yield this.web_view.highlight_search_terms(
|
||||
search_matches, cancellable
|
||||
);
|
||||
|
|
@ -848,6 +975,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
foreach (ContactFlowBoxChild address in this.searchable_addresses) {
|
||||
address.unmark_search_terms();
|
||||
}
|
||||
|
||||
if (this.web_view != null)
|
||||
this.web_view.unmark_search_terms();
|
||||
}
|
||||
|
||||
|
|
@ -1011,6 +1140,8 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
// returns HTML that is placed into the document in the position
|
||||
// where the MIME part was found
|
||||
private string? inline_image_replacer(Geary.RFC822.Part part) {
|
||||
if (this.web_view == null)
|
||||
initialize_web_view();
|
||||
Geary.Mime.ContentType content_type = part.content_type;
|
||||
if (content_type.media_type != "image" ||
|
||||
!this.web_view.can_show_mime_type(content_type.to_string())) {
|
||||
|
|
@ -1045,7 +1176,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
return "<img alt=\"%s\" class=\"%s\" src=\"%s%s\" />".printf(
|
||||
clean_filename,
|
||||
REPLACED_IMAGE_CLASS,
|
||||
ClientWebView.CID_URL_PREFIX,
|
||||
Components.WebView.CID_URL_PREFIX,
|
||||
Geary.HTML.escape_markup(id)
|
||||
);
|
||||
}
|
||||
|
|
@ -1059,7 +1190,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
this.load_remote_resources = true;
|
||||
this.remote_resources_requested = 0;
|
||||
this.remote_resources_loaded = 0;
|
||||
if (this.web_view != null) {
|
||||
this.web_view.load_remote_images();
|
||||
}
|
||||
if (update_email_flag) {
|
||||
flag_remote_images();
|
||||
}
|
||||
|
|
@ -1074,10 +1207,12 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
|
||||
if (placeholder != null) {
|
||||
this.body_placeholder = placeholder;
|
||||
if (this.web_view != null)
|
||||
this.web_view.hide();
|
||||
this.body_container.add(placeholder);
|
||||
show_message_body(true);
|
||||
} else {
|
||||
if (this.web_view != null)
|
||||
this.web_view.show();
|
||||
}
|
||||
}
|
||||
|
|
@ -1106,12 +1241,14 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
private void on_is_loading_notify() {
|
||||
if (this.web_view != null) {
|
||||
if (this.web_view.is_loading) {
|
||||
start_progress_loading();
|
||||
} else {
|
||||
stop_progress_loading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_resource_load_started(WebKit.WebView view,
|
||||
WebKit.WebResource res,
|
||||
|
|
@ -1369,7 +1506,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
|
|||
alt_text = (string) alt_maybe;
|
||||
}
|
||||
|
||||
if (uri.has_prefix(ClientWebView.CID_URL_PREFIX)) {
|
||||
if (uri.has_prefix(Components.WebView.CID_URL_PREFIX)) {
|
||||
// We can get the data directly from the attachment, so
|
||||
// don't bother getting it from the web view
|
||||
save_image(uri, alt_text, null);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
get; private set; default = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent web view created in this viewer.
|
||||
*
|
||||
* Keep the last created web view around so others can share the
|
||||
* same WebKitGTK WebProcess.
|
||||
*/
|
||||
internal ConversationWebView? previous_web_view { get; set; default = null; }
|
||||
|
||||
private Application.Configuration config;
|
||||
|
||||
private Gee.Set<Geary.App.Conversation>? selection_while_composing = null;
|
||||
|
|
@ -251,7 +259,10 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
Application.ContactStore contacts,
|
||||
bool start_mark_timer)
|
||||
throws GLib.Error {
|
||||
remove_current_list();
|
||||
// Keep the old ScrolledWindow around long enough for its
|
||||
// descendant web views to be kept so their WebProcess can be
|
||||
// re-used.
|
||||
var old_scroller = remove_current_list();
|
||||
|
||||
ConversationListBox new_list = new ConversationListBox(
|
||||
conversation,
|
||||
|
|
@ -297,6 +308,9 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
yield new_list.load_conversation(scroll_to, query);
|
||||
|
||||
// Not strictly necessary, but keeps the compiler happy
|
||||
old_scroller.destroy();
|
||||
}
|
||||
|
||||
// Add a new conversation list to the UI
|
||||
|
|
@ -316,7 +330,7 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
}
|
||||
|
||||
// Remove any existing conversation list, cancelling its loading
|
||||
private void remove_current_list() {
|
||||
private Gtk.ScrolledWindow remove_current_list() {
|
||||
if (this.find_cancellable != null) {
|
||||
this.find_cancellable.cancel();
|
||||
this.find_cancellable = null;
|
||||
|
|
@ -328,15 +342,17 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
|
|||
this.current_list = null;
|
||||
}
|
||||
|
||||
var old_scroller = this.conversation_scroller;
|
||||
// XXX GTK+ Bug 778190 workaround
|
||||
this.conversation_scroller.destroy(); // removes the list
|
||||
this.conversation_page.remove(old_scroller);
|
||||
new_conversation_scroller();
|
||||
return old_scroller;
|
||||
}
|
||||
|
||||
private void new_conversation_scroller() {
|
||||
// XXX Work around for GTK+ Bug 778190: Instead of replacing
|
||||
// the Viewport that contains the current list, replace the
|
||||
// complete ScrolledWindow. Need to put remove this method and
|
||||
// complete ScrolledWindow. Need to remove this method and
|
||||
// put the settings back into conversation-viewer.ui when we
|
||||
// can rely on it being fixed again.
|
||||
Gtk.ScrolledWindow scroller = new Gtk.ScrolledWindow(null, null);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
/*
|
||||
* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
* Copyright 2017 Michael Gratton <mike@vee.net>
|
||||
* Copyright © 2016 Software Freedom Conservancy Inc.
|
||||
* Copyright © 2017-2020 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.
|
||||
*/
|
||||
|
||||
public class ConversationWebView : ClientWebView {
|
||||
public class ConversationWebView : Components.WebView {
|
||||
|
||||
|
||||
private const string DECEPTIVE_LINK_CLICKED = "deceptiveLinkClicked";
|
||||
private const string DECEPTIVE_LINK_CLICKED = "deceptive_link_clicked";
|
||||
|
||||
// Key codes we don't forward on to the super class on key press
|
||||
// since we want to override them elsewhere, especially
|
||||
|
|
@ -41,10 +41,10 @@ public class ConversationWebView : ClientWebView {
|
|||
|
||||
public static new void load_resources()
|
||||
throws Error {
|
||||
ConversationWebView.app_script = ClientWebView.load_app_script(
|
||||
ConversationWebView.app_script = Components.WebView.load_app_script(
|
||||
"conversation-web-view.js"
|
||||
);
|
||||
ConversationWebView.app_stylesheet = ClientWebView.load_app_stylesheet(
|
||||
ConversationWebView.app_stylesheet = Components.WebView.load_app_stylesheet(
|
||||
"conversation-web-view.css"
|
||||
);
|
||||
}
|
||||
|
|
@ -56,36 +56,51 @@ public class ConversationWebView : ClientWebView {
|
|||
);
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new web view for displaying an email message body.
|
||||
*
|
||||
* A new WebKitGTK WebProcess will be constructed for this view.
|
||||
*/
|
||||
public ConversationWebView(Application.Configuration config) {
|
||||
base(config);
|
||||
init();
|
||||
|
||||
// These only need to be added when creating a new WebProcess,
|
||||
// not when sharing one
|
||||
this.user_content_manager.add_script(ConversationWebView.app_script);
|
||||
this.user_content_manager.add_style_sheet(ConversationWebView.app_stylesheet);
|
||||
}
|
||||
|
||||
register_message_handler(
|
||||
DECEPTIVE_LINK_CLICKED, on_deceptive_link_clicked
|
||||
);
|
||||
|
||||
this.notify["preferred-height"].connect(() => queue_resize());
|
||||
/**
|
||||
* Constructs a new web view for displaying an email message body.
|
||||
*
|
||||
* The WebKitGTK WebProcess will be shared with the related view's
|
||||
* process.
|
||||
*/
|
||||
internal ConversationWebView.with_related_view(
|
||||
Application.Configuration config,
|
||||
ConversationWebView related
|
||||
) {
|
||||
base.with_related_view(config, related);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current selection, for prefill as find text.
|
||||
*/
|
||||
public async string? get_selection_for_find() throws Error{
|
||||
JSC.Value result = yield call(
|
||||
Util.JS.callable("geary.getSelectionForFind"), null
|
||||
return yield call_returning<string?>(
|
||||
Util.JS.callable("getSelectionForFind"), null
|
||||
);
|
||||
return Util.JS.to_string(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current selection, for quoting in a message.
|
||||
*/
|
||||
public async string? get_selection_for_quoting() throws Error {
|
||||
JSC.Value result = yield call(
|
||||
Util.JS.callable("geary.getSelectionForQuoting"), null
|
||||
return yield call_returning<string?>(
|
||||
Util.JS.callable("getSelectionForQuoting"), null
|
||||
);
|
||||
return Util.JS.to_string(result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,10 +108,9 @@ public class ConversationWebView : ClientWebView {
|
|||
*/
|
||||
public async int? get_anchor_target_y(string anchor_body)
|
||||
throws GLib.Error {
|
||||
JSC.Value result = yield call(
|
||||
Util.JS.callable("geary.getAnchorTargetY").string(anchor_body), null
|
||||
return yield call_returning<int?>(
|
||||
Util.JS.callable("getAnchorTargetY").string(anchor_body), null
|
||||
);
|
||||
return (int) Util.JS.to_int32(result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -206,41 +220,48 @@ public class ConversationWebView : ClientWebView {
|
|||
minimum_height = natural_height = 0;
|
||||
}
|
||||
|
||||
private void on_deceptive_link_clicked(WebKit.JavascriptResult result) {
|
||||
try {
|
||||
JSC.Value object = result.get_js_value();
|
||||
uint reason = (uint) Util.JS.to_int32(
|
||||
Util.JS.get_property(object, "reason")
|
||||
private void init() {
|
||||
register_message_callback(
|
||||
DECEPTIVE_LINK_CLICKED, on_deceptive_link_clicked
|
||||
);
|
||||
|
||||
string href = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "href")
|
||||
);
|
||||
this.notify["preferred-height"].connect(() => queue_resize());
|
||||
}
|
||||
|
||||
string text = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "text")
|
||||
);
|
||||
private void on_deceptive_link_clicked(GLib.Variant? parameters) {
|
||||
var dict = new GLib.VariantDict(parameters);
|
||||
uint reason = (uint) dict.lookup_value(
|
||||
"reason", GLib.VariantType.DOUBLE
|
||||
).get_double();
|
||||
|
||||
JSC.Value js_location = Util.JS.get_property(object, "location");
|
||||
string href = dict.lookup_value(
|
||||
"href", GLib.VariantType.STRING
|
||||
).get_string();
|
||||
|
||||
string text = dict.lookup_value(
|
||||
"text", GLib.VariantType.STRING
|
||||
).get_string();
|
||||
|
||||
Gdk.Rectangle location = Gdk.Rectangle();
|
||||
location.x = Util.JS.to_int32(
|
||||
Util.JS.get_property(js_location, "x")
|
||||
);
|
||||
location.y = Util.JS.to_int32(
|
||||
Util.JS.get_property(js_location, "y")
|
||||
);
|
||||
location.width = Util.JS.to_int32(
|
||||
Util.JS.get_property(js_location, "width")
|
||||
);
|
||||
location.height = Util.JS.to_int32(
|
||||
Util.JS.get_property(js_location, "height")
|
||||
var location_dict = new GLib.VariantDict(
|
||||
dict.lookup_value("location", GLib.VariantType.VARDICT)
|
||||
);
|
||||
location.x = (int) location_dict.lookup_value(
|
||||
"x", GLib.VariantType.DOUBLE
|
||||
).get_double();
|
||||
location.y = (int) location_dict.lookup_value(
|
||||
"y", GLib.VariantType.DOUBLE
|
||||
).get_double();
|
||||
location.width = (int) location_dict.lookup_value(
|
||||
"width", GLib.VariantType.DOUBLE
|
||||
).get_double();
|
||||
location.height = (int) location_dict.lookup_value(
|
||||
"height", GLib.VariantType.DOUBLE
|
||||
).get_double();
|
||||
|
||||
deceptive_link_clicked((DeceptiveText) reason, text, href, location);
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get deceptive link param: %s", err.message);
|
||||
}
|
||||
deceptive_link_clicked(
|
||||
(DeceptiveText) reason, text, href, location
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ client_vala_sources = files(
|
|||
|
||||
'client-action.vala',
|
||||
|
||||
'components/client-web-view.vala',
|
||||
'components/components-attachment-pane.vala',
|
||||
'components/components-conversation-actions.vala',
|
||||
'components/components-conversation-action-bar.vala',
|
||||
|
|
@ -64,6 +63,7 @@ client_vala_sources = files(
|
|||
'components/components-reflow-box.c',
|
||||
'components/components-search-bar.vala',
|
||||
'components/components-validator.vala',
|
||||
'components/components-web-view.vala',
|
||||
'components/count-badge.vala',
|
||||
'components/folder-popover.vala',
|
||||
'components/icon-factory.vala',
|
||||
|
|
@ -191,16 +191,6 @@ client_vala_args += [
|
|||
)
|
||||
]
|
||||
|
||||
# Enable shared shecondary process if available.
|
||||
# See issues #558 and #559
|
||||
webkit_version = webkit2gtk.version().split('.')
|
||||
if webkit_version[0].to_int() <= 2 and webkit_version[1].to_int() <= 24
|
||||
message('Enabling WebKitGTK shared process model')
|
||||
client_vala_args += [
|
||||
'-D', 'HAS_WEBKIT_SHARED_PROC'
|
||||
]
|
||||
endif
|
||||
|
||||
# Main client application library
|
||||
client_lib = shared_library(
|
||||
client_package,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2017,2019 Michael James Gratton <mike@vee.net>
|
||||
* Copyright © 2017-2020 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.
|
||||
|
|
@ -25,6 +25,64 @@ namespace Util.JS {
|
|||
TYPE
|
||||
}
|
||||
|
||||
/** Supported types of JSC values. */
|
||||
public enum JscType {
|
||||
|
||||
/** Specifies an unsupported value type. */
|
||||
UNKNOWN,
|
||||
|
||||
/** Specifies a JavaScript `undefined` value. */
|
||||
UNDEFINED,
|
||||
|
||||
/** Specifies a JavaScript `null` value. */
|
||||
NULL,
|
||||
FUNCTION,
|
||||
STRING,
|
||||
NUMBER,
|
||||
BOOLEAN,
|
||||
ARRAY,
|
||||
CONSTRUCTOR,
|
||||
OBJECT;
|
||||
|
||||
/**
|
||||
* Determines the type of a JSC value.
|
||||
*
|
||||
* Returns the type of the given value, or {@link UNKNOWN} if
|
||||
* it could not be determined.
|
||||
*/
|
||||
public static JscType to_type(JSC.Value value) {
|
||||
if (value.is_undefined()) {
|
||||
return UNDEFINED;
|
||||
}
|
||||
if (value.is_null()) {
|
||||
return NULL;
|
||||
}
|
||||
if (value.is_string()) {
|
||||
return STRING;
|
||||
}
|
||||
if (value.is_number()) {
|
||||
return NUMBER;
|
||||
}
|
||||
if (value.is_boolean()) {
|
||||
return BOOLEAN;
|
||||
}
|
||||
if (value.is_array()) {
|
||||
return ARRAY;
|
||||
}
|
||||
if (value.is_object()) {
|
||||
return OBJECT;
|
||||
}
|
||||
if (value.is_function()) {
|
||||
return FUNCTION;
|
||||
}
|
||||
if (value.is_constructor()) {
|
||||
return CONSTRUCTOR;
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSC Value as a bool.
|
||||
*
|
||||
|
|
@ -128,53 +186,182 @@ namespace Util.JS {
|
|||
}
|
||||
|
||||
/**
|
||||
* Escapes a string so as to be safe to use as a JS string literal.
|
||||
* Converts a JS value to a GLib variant.
|
||||
*
|
||||
* This does not append opening or closing quotes.
|
||||
* Simple value objects (string, number, and Boolean values),
|
||||
* arrays of these, and objects with these types as properties are
|
||||
* supported. Arrays containing objects of the same type are
|
||||
* converted to arrays, otherwise they are converted to tuples,
|
||||
* empty arrays are converted to the unit tuple, and objects are
|
||||
* converted to vardict containing property names as keys and
|
||||
* values. Null and undefined values are returned as an empty
|
||||
* maybe variant type, since it is not possible to determine the
|
||||
* actual type.
|
||||
*
|
||||
* Throws a type error if the given value's type is not supported.
|
||||
*/
|
||||
public string escape_string(string value) {
|
||||
StringBuilder builder = new StringBuilder.sized(value.length);
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
if (value.valid_char(i)) {
|
||||
unichar c = value.get_char(i);
|
||||
switch (c) {
|
||||
case '\x00':
|
||||
builder.append("\x00");
|
||||
public inline GLib.Variant value_to_variant(JSC.Value value)
|
||||
throws Error {
|
||||
GLib.Variant? variant = null;
|
||||
switch (JscType.to_type(value)) {
|
||||
case UNDEFINED:
|
||||
case NULL:
|
||||
variant = new GLib.Variant.maybe(GLib.VariantType.VARIANT, null);
|
||||
break;
|
||||
case '\'':
|
||||
builder.append("\\\'");
|
||||
|
||||
case STRING:
|
||||
variant = new GLib.Variant.string(value.to_string());
|
||||
break;
|
||||
case '"':
|
||||
builder.append("\\\"");
|
||||
|
||||
case NUMBER:
|
||||
variant = new GLib.Variant.double(value.to_double());
|
||||
break;
|
||||
case '\\':
|
||||
builder.append("\\\\");
|
||||
|
||||
case BOOLEAN:
|
||||
variant = new GLib.Variant.boolean(value.to_boolean());
|
||||
break;
|
||||
case '\n':
|
||||
builder.append("\\n");
|
||||
|
||||
case ARRAY:
|
||||
int len = to_int32(value.object_get_property("length"));
|
||||
if (len == 0) {
|
||||
variant = new GLib.Variant.tuple({});
|
||||
} else {
|
||||
JSC.Value element = value.object_get_property_at_index(0);
|
||||
var first_type = JscType.to_type(element);
|
||||
var all_same_type = true;
|
||||
var values = new GLib.Variant[len];
|
||||
values[0] = value_to_variant(element);
|
||||
for (int i = 1; i < len; i++) {
|
||||
element = value.object_get_property_at_index(i);
|
||||
values[i] = value_to_variant(element);
|
||||
all_same_type &= (first_type == JscType.to_type(element));
|
||||
}
|
||||
if (!all_same_type) {
|
||||
variant = new GLib.Variant.tuple(values);
|
||||
} else {
|
||||
variant = new GLib.Variant.array(
|
||||
values[0].get_type(), values
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '\r':
|
||||
builder.append("\\r");
|
||||
break;
|
||||
case '\x0b':
|
||||
builder.append("\x0b");
|
||||
break;
|
||||
case '\t':
|
||||
builder.append("\\t");
|
||||
break;
|
||||
case '\b':
|
||||
builder.append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.append("\\f");
|
||||
|
||||
case OBJECT:
|
||||
GLib.VariantDict dict = new GLib.VariantDict();
|
||||
string[] names = value.object_enumerate_properties();
|
||||
if (names != null) {
|
||||
foreach (var name in names) {
|
||||
dict.insert_value(
|
||||
name,
|
||||
value_to_variant(value.object_get_property(name))
|
||||
);
|
||||
}
|
||||
}
|
||||
variant = dict.end();
|
||||
break;
|
||||
|
||||
default:
|
||||
builder.append_unichar(c);
|
||||
break;
|
||||
throw new Error.TYPE("Unsupported JS type: %s", value.to_string());
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GLib variant to a JS value.
|
||||
*
|
||||
* Simple value objects (string, number, and Boolean values),
|
||||
* arrays and tuples of these, and dictionaries with string keys
|
||||
* are supported. Tuples and arrays are converted to JS arrays,
|
||||
* and dictionaries or tuples containing dictionary entries are
|
||||
* converted to JS objects.
|
||||
*
|
||||
* Throws a type error if the given variant's type is not supported.
|
||||
*/
|
||||
public inline JSC.Value variant_to_value(JSC.Context context,
|
||||
GLib.Variant variant)
|
||||
throws Error.TYPE {
|
||||
JSC.Value? value = null;
|
||||
GLib.Variant.Class type = variant.classify();
|
||||
if (type == MAYBE) {
|
||||
GLib.Variant? maybe = variant.get_maybe();
|
||||
if (maybe != null) {
|
||||
value = variant_to_value(context, maybe);
|
||||
} else {
|
||||
value = new JSC.Value.null(context);
|
||||
}
|
||||
} else if (type == VARIANT) {
|
||||
value = variant_to_value(context, variant.get_variant());
|
||||
} else if (type == STRING) {
|
||||
value = new JSC.Value.string(context, variant.get_string());
|
||||
} else if (type == BOOLEAN) {
|
||||
value = new JSC.Value.boolean(context, variant.get_boolean());
|
||||
} else if (type == DOUBLE) {
|
||||
value = new JSC.Value.number(context, variant.get_double());
|
||||
} else if (type == INT64) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_int64());
|
||||
} else if (type == INT32) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_int32());
|
||||
} else if (type == INT16) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_int16());
|
||||
} else if (type == UINT64) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_uint64());
|
||||
} else if (type == UINT32) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_uint32());
|
||||
} else if (type == UINT16) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_uint16());
|
||||
} else if (type == BYTE) {
|
||||
value = new JSC.Value.number(context, (double) variant.get_byte());
|
||||
} else if (type == ARRAY ||
|
||||
type == TUPLE) {
|
||||
size_t len = variant.n_children();
|
||||
if (len == 0) {
|
||||
if (type == ARRAY ||
|
||||
type == TUPLE) {
|
||||
value = new JSC.Value.array_from_garray(context, null);
|
||||
} else {
|
||||
value = new JSC.Value.object(context, null, null);
|
||||
}
|
||||
} else {
|
||||
var first = variant.get_child_value(0);
|
||||
if (first.classify() == DICT_ENTRY) {
|
||||
value = new JSC.Value.object(context, null, null);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
var entry = variant.get_child_value(i);
|
||||
if (entry.classify() != DICT_ENTRY) {
|
||||
throw new Error.TYPE(
|
||||
"Variant mixes dict entries with others: %s",
|
||||
variant.print(true)
|
||||
);
|
||||
}
|
||||
var key = entry.get_child_value(0);
|
||||
if (key.classify() != STRING) {
|
||||
throw new Error.TYPE(
|
||||
"Dict entry key is not a string: %s",
|
||||
entry.print(true)
|
||||
);
|
||||
}
|
||||
value.object_set_property(
|
||||
key.get_string(),
|
||||
variant_to_value(context, entry.get_child_value(1))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
var values = new GLib.GenericArray<JSC.Value>((uint) len);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
values.add(
|
||||
variant_to_value(context, variant.get_child_value(i))
|
||||
);
|
||||
}
|
||||
value = new JSC.Value.array_from_garray(context, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (string) builder.data;
|
||||
if (value == null) {
|
||||
throw new Error.TYPE(
|
||||
"Unsupported variant type %s", variant.print(true)
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -189,40 +376,54 @@ namespace Util.JS {
|
|||
*/
|
||||
public class Callable {
|
||||
|
||||
private string base_name;
|
||||
private string[] safe_args = new string[0];
|
||||
private string name;
|
||||
private GLib.Variant[] args = {};
|
||||
|
||||
|
||||
public Callable(string base_name) {
|
||||
this.base_name = base_name;
|
||||
public Callable(string name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public WebKit.UserMessage to_message() {
|
||||
GLib.Variant? args = null;
|
||||
if (this.args.length == 1) {
|
||||
args = this.args[0];
|
||||
} else if (this.args.length > 1) {
|
||||
args = new GLib.Variant.tuple(this.args);
|
||||
}
|
||||
return new WebKit.UserMessage(this.name, args);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return base_name + "(" + global::string.joinv(",", safe_args) + ");";
|
||||
string[] args = new string[this.args.length];
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
args[i] = this.args[i].print(true);
|
||||
}
|
||||
return this.name + "(" + global::string.joinv(",", args) + ")";
|
||||
}
|
||||
|
||||
public Callable string(string value) {
|
||||
add_param("\"" + escape_string(value) + "\"");
|
||||
add_param(new GLib.Variant.string(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Callable double(double value) {
|
||||
add_param(value.to_string());
|
||||
add_param(new GLib.Variant.double(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Callable int(int value) {
|
||||
add_param(value.to_string());
|
||||
add_param(new GLib.Variant.int32(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Callable bool(bool value) {
|
||||
add_param(value ? "true" : "false");
|
||||
add_param(new GLib.Variant.boolean(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
private inline void add_param(string value) {
|
||||
this.safe_args += value;
|
||||
private inline void add_param(GLib.Variant value) {
|
||||
this.args += value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2016 Michael Gratton <mike@vee.net>
|
||||
* Copyright © 2016-2020 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.
|
||||
|
|
@ -13,9 +13,9 @@ public void webkit_web_extension_initialize_with_user_data(WebKit.WebExtension e
|
|||
bool logging_enabled = data.get_boolean();
|
||||
|
||||
Geary.Logging.init();
|
||||
GLib.Log.set_writer_func(Geary.Logging.default_log_writer);
|
||||
if (logging_enabled) {
|
||||
Geary.Logging.log_to(stdout);
|
||||
GLib.Log.set_writer_func(Geary.Logging.default_log_writer);
|
||||
Geary.Logging.log_to(GLib.stdout);
|
||||
}
|
||||
|
||||
debug("Initialising...");
|
||||
|
|
@ -30,27 +30,26 @@ public void webkit_web_extension_initialize_with_user_data(WebKit.WebExtension e
|
|||
*/
|
||||
public class GearyWebExtension : Object {
|
||||
|
||||
private const string PAGE_STATE_OBJECT_NAME = "geary";
|
||||
|
||||
// Keep these in sync with Components.WebView
|
||||
private const string MESSAGE_RETURN_VALUE_NAME = "__return__";
|
||||
private const string MESSAGE_EXCEPTION_NAME = "__exception__";
|
||||
|
||||
private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data", "blob" };
|
||||
|
||||
private const string EXTENSION_CLASS_VAR = "_GearyWebExtension";
|
||||
private const string EXTENSION_CLASS_SEND = "send";
|
||||
private const string REMOTE_LOAD_VAR = "_gearyAllowRemoteResourceLoads";
|
||||
|
||||
private WebKit.WebExtension extension;
|
||||
|
||||
|
||||
public GearyWebExtension(WebKit.WebExtension extension) {
|
||||
this.extension = extension;
|
||||
extension.page_created.connect((extension, web_page) => {
|
||||
web_page.console_message_sent.connect(on_console_message);
|
||||
web_page.send_request.connect(on_send_request);
|
||||
// XXX investigate whether the earliest supported
|
||||
// version of WK supports the DOM "selectionchanged"
|
||||
// event, and if so use that rather that doing it in
|
||||
// here in the extension
|
||||
web_page.get_editor().selection_changed.connect(() => {
|
||||
selection_changed(web_page);
|
||||
});
|
||||
});
|
||||
extension.page_created.connect(on_page_created);
|
||||
}
|
||||
|
||||
// XXX Conditionally enable while we still depend on WK2 <2.12
|
||||
private void on_console_message(WebKit.WebPage page,
|
||||
WebKit.ConsoleMessage message) {
|
||||
string source = message.get_source_id();
|
||||
|
|
@ -77,7 +76,10 @@ public class GearyWebExtension : Object {
|
|||
if (should_load_remote_images(page)) {
|
||||
should_load = true;
|
||||
} else {
|
||||
remote_image_load_blocked(page);
|
||||
page.send_message_to_view.begin(
|
||||
new WebKit.UserMessage("remote_image_load_blocked", null),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,14 +91,7 @@ public class GearyWebExtension : Object {
|
|||
WebKit.Frame frame = page.get_main_frame();
|
||||
JSC.Context context = frame.get_js_context();
|
||||
try {
|
||||
JSC.Value ret = execute_script(
|
||||
context,
|
||||
"geary.allowRemoteImages",
|
||||
GLib.Log.FILE,
|
||||
GLib.Log.METHOD,
|
||||
GLib.Log.LINE
|
||||
);
|
||||
should_load = Util.JS.to_bool(ret);
|
||||
should_load = Util.JS.to_bool(context.get_value(REMOTE_LOAD_VAR));
|
||||
} catch (GLib.Error err) {
|
||||
debug(
|
||||
"Error checking PageState::allowRemoteImages: %s",
|
||||
|
|
@ -106,52 +101,172 @@ public class GearyWebExtension : Object {
|
|||
return should_load;
|
||||
}
|
||||
|
||||
private void remote_image_load_blocked(WebKit.WebPage page) {
|
||||
private WebKit.UserMessage to_exception_message(string? name,
|
||||
string? message,
|
||||
string? backtrace = null,
|
||||
string? source = null,
|
||||
int line_number = -1,
|
||||
int column_number = -1) {
|
||||
var detail = new GLib.VariantDict();
|
||||
if (name != null) {
|
||||
detail.insert_value("name", new GLib.Variant.string(name));
|
||||
}
|
||||
if (message != null) {
|
||||
detail.insert_value("message", new GLib.Variant.string(message));
|
||||
}
|
||||
if (backtrace != null) {
|
||||
detail.insert_value("backtrace", new GLib.Variant.string(backtrace));
|
||||
}
|
||||
if (source != null) {
|
||||
detail.insert_value("source", new GLib.Variant.string(source));
|
||||
}
|
||||
if (line_number > 0) {
|
||||
detail.insert_value("line_number", new GLib.Variant.uint32(line_number));
|
||||
}
|
||||
if (column_number > 0) {
|
||||
detail.insert_value("column_number", new GLib.Variant.uint32(column_number));
|
||||
}
|
||||
return new WebKit.UserMessage(
|
||||
MESSAGE_EXCEPTION_NAME,
|
||||
detail.end()
|
||||
);
|
||||
}
|
||||
|
||||
private void on_page_created(WebKit.WebExtension extension,
|
||||
WebKit.WebPage page) {
|
||||
WebKit.Frame frame = page.get_main_frame();
|
||||
JSC.Context context = frame.get_js_context();
|
||||
try {
|
||||
execute_script(
|
||||
context,
|
||||
"geary.remoteImageLoadBlocked();",
|
||||
GLib.Log.FILE,
|
||||
GLib.Log.METHOD,
|
||||
GLib.Log.LINE
|
||||
|
||||
var extension_class = context.register_class(
|
||||
this.get_type().name(),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
} catch (Error err) {
|
||||
debug(
|
||||
"Error calling PageState::remoteImageLoadBlocked: %s",
|
||||
err.message
|
||||
extension_class.add_method(
|
||||
EXTENSION_CLASS_SEND,
|
||||
(instance, values) => {
|
||||
return this.on_page_send_message(page, values);
|
||||
},
|
||||
GLib.Type.NONE
|
||||
);
|
||||
}
|
||||
context.set_value(
|
||||
EXTENSION_CLASS_VAR,
|
||||
new JSC.Value.object(context, extension_class, extension_class)
|
||||
);
|
||||
|
||||
context.set_value(
|
||||
REMOTE_LOAD_VAR,
|
||||
new JSC.Value.boolean(context, false)
|
||||
);
|
||||
|
||||
page.console_message_sent.connect(on_console_message);
|
||||
page.send_request.connect(on_send_request);
|
||||
page.user_message_received.connect(on_page_message_received);
|
||||
}
|
||||
|
||||
private void selection_changed(WebKit.WebPage page) {
|
||||
private bool on_page_message_received(WebKit.WebPage page,
|
||||
WebKit.UserMessage message) {
|
||||
WebKit.Frame frame = page.get_main_frame();
|
||||
JSC.Context context = frame.get_js_context();
|
||||
JSC.Value page_state = context.get_value(PAGE_STATE_OBJECT_NAME);
|
||||
|
||||
try {
|
||||
execute_script(
|
||||
JSC.Value[]? call_param = null;
|
||||
GLib.Variant? message_param = message.parameters;
|
||||
if (message_param != null) {
|
||||
if (message_param.is_container()) {
|
||||
size_t len = message_param.n_children();
|
||||
call_param = new JSC.Value[len];
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
call_param[i] = Util.JS.variant_to_value(
|
||||
context,
|
||||
"geary.selectionChanged();",
|
||||
GLib.Log.FILE,
|
||||
GLib.Log.METHOD,
|
||||
GLib.Log.LINE
|
||||
message_param.get_child_value(i)
|
||||
);
|
||||
} catch (Error err) {
|
||||
debug("Error calling PageStates::selectionChanged: %s", err.message);
|
||||
}
|
||||
} else {
|
||||
call_param = {
|
||||
Util.JS.variant_to_value(context, message_param)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private JSC.Value execute_script(JSC.Context context,
|
||||
string script,
|
||||
string file_name,
|
||||
string method_name,
|
||||
int line_number)
|
||||
throws Util.JS.Error {
|
||||
JSC.Value ret = context.evaluate_with_source_uri(
|
||||
script, -1, "geary:%s/%s".printf(file_name, method_name), line_number
|
||||
JSC.Value ret = page_state.object_invoke_methodv(
|
||||
message.name, call_param
|
||||
);
|
||||
Util.JS.check_exception(context);
|
||||
return ret;
|
||||
|
||||
// Must send a reply, even for void calls, otherwise
|
||||
// WebKitGTK will complain. So return a message return
|
||||
// rain hail or shine.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=215880
|
||||
|
||||
JSC.Exception? thrown = context.get_exception();
|
||||
if (thrown != null) {
|
||||
message.send_reply(
|
||||
to_exception_message(
|
||||
thrown.get_name(),
|
||||
thrown.get_message(),
|
||||
thrown.get_backtrace_string(),
|
||||
thrown.get_source_uri(),
|
||||
(int) thrown.get_line_number(),
|
||||
(int) thrown.get_column_number()
|
||||
)
|
||||
);
|
||||
} else {
|
||||
message.send_reply(
|
||||
new WebKit.UserMessage(
|
||||
MESSAGE_RETURN_VALUE_NAME,
|
||||
Util.JS.value_to_variant(ret)
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (GLib.Error err) {
|
||||
debug("Failed to handle message: %s", err.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool on_page_send_message(WebKit.WebPage page,
|
||||
GLib.GenericArray<JSC.Value> args) {
|
||||
WebKit.UserMessage? message = null;
|
||||
if (args.length > 0) {
|
||||
var name = args.get(0).to_string();
|
||||
GLib.Variant? parameters = null;
|
||||
if (args.length > 1) {
|
||||
JSC.Value param_value = args.get(1);
|
||||
try {
|
||||
int len = Util.JS.to_int32(
|
||||
param_value.object_get_property("length")
|
||||
);
|
||||
if (len == 1) {
|
||||
parameters = Util.JS.value_to_variant(
|
||||
param_value.object_get_property_at_index(0)
|
||||
);
|
||||
} else if (len > 1) {
|
||||
parameters = Util.JS.value_to_variant(param_value);
|
||||
}
|
||||
} catch (Util.JS.Error err) {
|
||||
message = to_exception_message(
|
||||
this.get_type().name(), err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
if (message == null) {
|
||||
message = new WebKit.UserMessage(name, parameters);
|
||||
}
|
||||
}
|
||||
if (message == null) {
|
||||
var log_message = "Not enough parameters for JS call to %s.%s()".printf(
|
||||
EXTENSION_CLASS_VAR,
|
||||
EXTENSION_CLASS_SEND
|
||||
);
|
||||
debug(log_message);
|
||||
message = to_exception_message(this.get_type().name(), log_message);
|
||||
}
|
||||
|
||||
page.send_message_to_view.begin(message, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,35 +6,42 @@
|
|||
*/
|
||||
|
||||
|
||||
public abstract class ClientWebViewTestCase<V> : TestCase {
|
||||
public abstract class Components.WebViewTestCase<V> : TestCase {
|
||||
|
||||
protected V? test_view = null;
|
||||
protected Application.Configuration? config = null;
|
||||
|
||||
protected ClientWebViewTestCase(string name) {
|
||||
protected WebViewTestCase(string name) {
|
||||
base(name);
|
||||
}
|
||||
|
||||
public override void set_up() {
|
||||
this.config = new Application.Configuration(Application.Client.SCHEMA_ID);
|
||||
this.config.enable_debug = true;
|
||||
ClientWebView.init_web_context(
|
||||
|
||||
WebView.init_web_context(
|
||||
this.config,
|
||||
File.new_for_path(_BUILD_ROOT_DIR).get_child("src"),
|
||||
File.new_for_path("/tmp") // XXX use something better here
|
||||
);
|
||||
try {
|
||||
ClientWebView.load_resources(GLib.File.new_for_path("/tmp"));
|
||||
WebView.load_resources(GLib.File.new_for_path("/tmp"));
|
||||
} catch (GLib.Error err) {
|
||||
GLib.assert_not_reached();
|
||||
}
|
||||
|
||||
this.test_view = set_up_test_view();
|
||||
}
|
||||
|
||||
public override void set_up() {
|
||||
this.test_view = set_up_test_view();
|
||||
protected override void tear_down() {
|
||||
this.config = null;
|
||||
this.test_view = null;
|
||||
}
|
||||
|
||||
protected abstract V set_up_test_view();
|
||||
|
||||
protected virtual void load_body_fixture(string html = "") {
|
||||
ClientWebView client_view = (ClientWebView) this.test_view;
|
||||
WebView client_view = (WebView) this.test_view;
|
||||
client_view.load_html(html);
|
||||
while (!client_view.is_content_loaded) {
|
||||
Gtk.main_iteration();
|
||||
|
|
@ -42,7 +49,7 @@ public abstract class ClientWebViewTestCase<V> : TestCase {
|
|||
}
|
||||
|
||||
protected WebKit.JavascriptResult run_javascript(string command) throws Error {
|
||||
ClientWebView view = (ClientWebView) this.test_view;
|
||||
WebView view = (WebView) this.test_view;
|
||||
view.run_javascript.begin(command, null, this.async_completion);
|
||||
return view.run_javascript.end(async_result());
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class ClientWebViewTest : TestCase {
|
||||
public class Components.WebViewTest : TestCase {
|
||||
|
||||
public ClientWebViewTest() {
|
||||
base("ClientWebViewTest");
|
||||
public WebViewTest() {
|
||||
base("Components.WebViewTest");
|
||||
add_test("init_web_context", init_web_context);
|
||||
add_test("load_resources", load_resources);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ public class ClientWebViewTest : TestCase {
|
|||
Application.Client.SCHEMA_ID
|
||||
);
|
||||
config.enable_debug = true;
|
||||
ClientWebView.init_web_context(
|
||||
WebView.init_web_context(
|
||||
config,
|
||||
File.new_for_path(_BUILD_ROOT_DIR).get_child("src"),
|
||||
File.new_for_path("/tmp") // XXX use something better here
|
||||
|
|
@ -27,7 +27,7 @@ public class ClientWebViewTest : TestCase {
|
|||
|
||||
public void load_resources() throws GLib.Error {
|
||||
try {
|
||||
ClientWebView.load_resources(GLib.File.new_for_path("/tmp"));
|
||||
WebView.load_resources(GLib.File.new_for_path("/tmp"));
|
||||
} catch (GLib.Error err) {
|
||||
assert_not_reached();
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class Composer.WebViewTest : ClientWebViewTestCase<Composer.WebView> {
|
||||
public class Composer.WebViewTest : Components.WebViewTestCase<Composer.WebView> {
|
||||
|
||||
|
||||
public WebViewTest() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2017 Michael Gratton <mike@vee.net>
|
||||
* Copyright © 2017-2020 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.
|
||||
|
|
@ -7,18 +7,152 @@
|
|||
|
||||
public class Util.JS.Test : TestCase {
|
||||
|
||||
|
||||
private JSC.Context? context = null;
|
||||
|
||||
|
||||
public Test() {
|
||||
base("Util.JS.Test");
|
||||
add_test("escape_string", escape_string);
|
||||
add_test("to_variant", to_variant);
|
||||
add_test("to_value", to_value);
|
||||
}
|
||||
|
||||
public void escape_string() throws GLib.Error {
|
||||
assert(Util.JS.escape_string("\n") == """\n""");
|
||||
assert(Util.JS.escape_string("\r") == """\r""");
|
||||
assert(Util.JS.escape_string("\t") == """\t""");
|
||||
assert(Util.JS.escape_string("\'") == """\'""");
|
||||
assert(Util.JS.escape_string("\"") == """\"""");
|
||||
public override void set_up() throws GLib.Error {
|
||||
this.context = new JSC.Context();
|
||||
}
|
||||
|
||||
assert(Util.JS.escape_string("something…\n") == """something…\n""");
|
||||
public override void tear_down() throws GLib.Error {
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
public void to_variant() throws GLib.Error {
|
||||
assert_equal(
|
||||
value_to_variant(new JSC.Value.null(this.context)).print(true),
|
||||
"@mv nothing"
|
||||
);
|
||||
assert_equal(
|
||||
value_to_variant(new JSC.Value.string(this.context, "test")).print(true),
|
||||
"'test'"
|
||||
);
|
||||
assert_equal(
|
||||
value_to_variant(new JSC.Value.number(this.context, 1.0)).print(true),
|
||||
"1.0"
|
||||
);
|
||||
assert_equal(
|
||||
value_to_variant(new JSC.Value.boolean(this.context, true)).print(true),
|
||||
"true"
|
||||
);
|
||||
assert_equal(
|
||||
value_to_variant(new JSC.Value.boolean(this.context, false)).print(true),
|
||||
"false"
|
||||
);
|
||||
|
||||
var value = new JSC.Value.array_from_garray(this.context, null);
|
||||
assert_equal(
|
||||
value_to_variant(value).print(true),
|
||||
"()"
|
||||
);
|
||||
|
||||
var array = new GLib.GenericArray<JSC.Value>();
|
||||
array.add(new JSC.Value.string(this.context, "test"));
|
||||
value = new JSC.Value.array_from_garray(this.context, array);
|
||||
assert_equal(
|
||||
value_to_variant(value).print(true),
|
||||
"['test']"
|
||||
);
|
||||
|
||||
array = new GLib.GenericArray<JSC.Value>();
|
||||
array.add(new JSC.Value.string(this.context, "test1"));
|
||||
array.add(new JSC.Value.string(this.context, "test2"));
|
||||
value = new JSC.Value.array_from_garray(this.context, array);
|
||||
assert_equal(
|
||||
value_to_variant(value).print(true),
|
||||
"['test1', 'test2']"
|
||||
);
|
||||
|
||||
array = new GLib.GenericArray<JSC.Value>();
|
||||
array.add(new JSC.Value.string(this.context, "test"));
|
||||
array.add(new JSC.Value.boolean(this.context, true));
|
||||
value = new JSC.Value.array_from_garray(this.context, array);
|
||||
assert_equal(
|
||||
value_to_variant(value).print(true),
|
||||
"('test', true)"
|
||||
);
|
||||
|
||||
value = new JSC.Value.object(this.context, null, null);
|
||||
assert_equal(
|
||||
value_to_variant(value).print(true),
|
||||
"@a{sv} {}"
|
||||
);
|
||||
value.object_set_property(
|
||||
"test", new JSC.Value.boolean(this.context, true)
|
||||
);
|
||||
assert_equal(
|
||||
value_to_variant(value).print(true),
|
||||
"{'test': <true>}"
|
||||
);
|
||||
}
|
||||
|
||||
public void to_value() throws GLib.Error {
|
||||
var variant = new GLib.Variant.maybe(GLib.VariantType.STRING, null);
|
||||
var value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_null(), variant.print(true));
|
||||
|
||||
variant = new GLib.Variant.string("test");
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_string(), variant.print(true));
|
||||
assert_equal(value.to_string(), "test", variant.print(true));
|
||||
|
||||
variant = new GLib.Variant.int32(42);
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_number(), variant.print(true));
|
||||
assert_equal<int32?>(value.to_int32(), 42, variant.print(true));
|
||||
|
||||
variant = new GLib.Variant.double(42.0);
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_number(), variant.print(true));
|
||||
assert_within(value.to_double(), 42.0, 0.0000001, variant.print(true));
|
||||
|
||||
variant = new GLib.Variant.boolean(true);
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_boolean(), variant.print(true));
|
||||
assert_true(value.to_boolean(), variant.print(true));
|
||||
|
||||
variant = new GLib.Variant.boolean(false);
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_boolean(), variant.print(true));
|
||||
assert_false(value.to_boolean(), variant.print(true));
|
||||
|
||||
variant = new GLib.Variant.strv({"test"});
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_array(), variant.print(true));
|
||||
assert_true(
|
||||
value.object_get_property_at_index(0).is_string(),
|
||||
variant.print(true)
|
||||
);
|
||||
assert_equal(
|
||||
value.object_get_property_at_index(0).to_string(),
|
||||
"test",
|
||||
variant.print(true)
|
||||
);
|
||||
|
||||
var dict = new GLib.VariantDict();
|
||||
variant = dict.end();
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_object(), variant.print(true));
|
||||
|
||||
dict = new GLib.VariantDict();
|
||||
dict.insert_value("test", new GLib.Variant.boolean(true));
|
||||
variant = dict.end();
|
||||
value = variant_to_value(this.context, variant);
|
||||
assert_true(value.is_object(), variant.print(true));
|
||||
assert_true(
|
||||
value.object_get_property("test").is_boolean(),
|
||||
value.to_string()
|
||||
);
|
||||
assert_true(
|
||||
value.object_get_property("test").to_boolean(),
|
||||
value.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
class ClientPageStateTest : ClientWebViewTestCase<ClientWebView> {
|
||||
|
||||
|
||||
private class TestClientWebView : ClientWebView {
|
||||
|
||||
public TestClientWebView(Application.Configuration config) {
|
||||
base(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public ClientPageStateTest() {
|
||||
base("ClientPageStateTest");
|
||||
add_test("content_loaded", content_loaded);
|
||||
|
||||
try {
|
||||
ClientWebView.load_resources(GLib.File.new_for_path("/tmp"));
|
||||
} catch (GLib.Error err) {
|
||||
GLib.assert_not_reached();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void content_loaded() throws Error {
|
||||
bool content_loaded_triggered = false;
|
||||
this.test_view.content_loaded.connect(() => {
|
||||
content_loaded_triggered = true;
|
||||
});
|
||||
|
||||
assert(!this.test_view.is_content_loaded);
|
||||
|
||||
// XXX sketchy - this call will never return if the thing we
|
||||
// are testing does not work
|
||||
load_body_fixture("OHHAI");
|
||||
|
||||
assert(this.test_view.is_content_loaded);
|
||||
assert(content_loaded_triggered);
|
||||
}
|
||||
|
||||
protected override ClientWebView set_up_test_view() {
|
||||
WebKit.UserScript test_script;
|
||||
test_script = new WebKit.UserScript(
|
||||
"var geary = new PageState()",
|
||||
WebKit.UserContentInjectedFrames.TOP_FRAME,
|
||||
WebKit.UserScriptInjectionTime.START,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
ClientWebView view = new TestClientWebView(this.config);
|
||||
view.get_user_content_manager().add_script(test_script);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
168
test/js/components-page-state-test.vala
Normal file
168
test/js/components-page-state-test.vala
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
class Components.PageStateTest : WebViewTestCase<WebView> {
|
||||
|
||||
|
||||
private class TestWebView : Components.WebView {
|
||||
|
||||
public TestWebView(Application.Configuration config) {
|
||||
base(config);
|
||||
}
|
||||
|
||||
public new async void call_void(Util.JS.Callable callable)
|
||||
throws GLib.Error {
|
||||
yield base.call_void(callable, null);
|
||||
}
|
||||
|
||||
public new async string call_returning(Util.JS.Callable callable)
|
||||
throws GLib.Error {
|
||||
return yield base.call_returning<string>(callable, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public PageStateTest() {
|
||||
base("Components.PageStateTest");
|
||||
add_test("content_loaded", content_loaded);
|
||||
add_test("call_void", call_void);
|
||||
add_test("call_void_throws", call_void_throws);
|
||||
add_test("call_returning", call_returning);
|
||||
add_test("call_returning_throws", call_returning_throws);
|
||||
|
||||
try {
|
||||
WebView.load_resources(GLib.File.new_for_path("/tmp"));
|
||||
} catch (GLib.Error err) {
|
||||
GLib.assert_not_reached();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void content_loaded() throws Error {
|
||||
bool content_loaded_triggered = false;
|
||||
this.test_view.content_loaded.connect(() => {
|
||||
content_loaded_triggered = true;
|
||||
});
|
||||
|
||||
assert(!this.test_view.is_content_loaded);
|
||||
|
||||
// XXX sketchy - this call will never return if the thing we
|
||||
// are testing does not work
|
||||
load_body_fixture("OHHAI");
|
||||
|
||||
assert(this.test_view.is_content_loaded);
|
||||
assert(content_loaded_triggered);
|
||||
}
|
||||
|
||||
public void call_void() throws GLib.Error {
|
||||
load_body_fixture("OHHAI");
|
||||
var test_article = this.test_view as TestWebView;
|
||||
|
||||
test_article.call_void.begin(
|
||||
new Util.JS.Callable("testVoid"), this.async_completion
|
||||
);
|
||||
test_article.call_void.end(this.async_result());
|
||||
assert_test_result("void");
|
||||
}
|
||||
|
||||
public void call_void_throws() throws GLib.Error {
|
||||
load_body_fixture("OHHAI");
|
||||
var test_article = this.test_view as TestWebView;
|
||||
|
||||
try {
|
||||
test_article.call_void.begin(
|
||||
new Util.JS.Callable("testThrow").string("void message"),
|
||||
this.async_completion
|
||||
);
|
||||
test_article.call_void.end(this.async_result());
|
||||
assert_not_reached();
|
||||
} catch (Util.JS.Error.EXCEPTION err) {
|
||||
assert_string(
|
||||
err.message
|
||||
).contains(
|
||||
"testThrow"
|
||||
// WebKitGTK doesn't actually pass any details through:
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=215877
|
||||
// ).contains(
|
||||
// "Error"
|
||||
// ).contains(
|
||||
// "void message"
|
||||
// ).contains(
|
||||
// "components-web-view.js"
|
||||
);
|
||||
assert_test_result("void message");
|
||||
}
|
||||
}
|
||||
|
||||
public void call_returning() throws GLib.Error {
|
||||
load_body_fixture("OHHAI");
|
||||
var test_article = this.test_view as TestWebView;
|
||||
|
||||
test_article.call_returning.begin(
|
||||
new Util.JS.Callable("testReturn").string("check 1-2"),
|
||||
this.async_completion
|
||||
);
|
||||
string ret = test_article.call_returning.end(this.async_result());
|
||||
assert_equal(ret, "check 1-2");
|
||||
assert_test_result("check 1-2");
|
||||
}
|
||||
|
||||
public void call_returning_throws() throws GLib.Error {
|
||||
load_body_fixture("OHHAI");
|
||||
var test_article = this.test_view as TestWebView;
|
||||
|
||||
try {
|
||||
test_article.call_returning.begin(
|
||||
new Util.JS.Callable("testThrow").string("return message"),
|
||||
this.async_completion
|
||||
);
|
||||
test_article.call_returning.end(this.async_result());
|
||||
assert_not_reached();
|
||||
} catch (Util.JS.Error.EXCEPTION err) {
|
||||
assert_string(
|
||||
err.message
|
||||
).contains(
|
||||
"testThrow"
|
||||
// WebKitGTK doesn't actually pass any details through:
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=215877
|
||||
// ).contains(
|
||||
// "Error"
|
||||
// ).contains(
|
||||
// "return message"
|
||||
// ).contains(
|
||||
// "components-web-view.js"
|
||||
);
|
||||
assert_test_result("return message");
|
||||
}
|
||||
}
|
||||
|
||||
protected override WebView set_up_test_view() {
|
||||
WebKit.UserScript test_script;
|
||||
test_script = new WebKit.UserScript(
|
||||
"var geary = new PageState()",
|
||||
WebKit.UserContentInjectedFrames.TOP_FRAME,
|
||||
WebKit.UserScriptInjectionTime.START,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
WebView view = new TestWebView(this.config);
|
||||
view.get_user_content_manager().add_script(test_script);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void assert_test_result(string expected)
|
||||
throws GLib.Error {
|
||||
string? result = Util.JS.to_string(
|
||||
run_javascript("geary.testResult")
|
||||
.get_js_value()
|
||||
);
|
||||
assert_equal(result, expected);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,13 +5,13 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
class Composer.PageStateTest : ClientWebViewTestCase<Composer.WebView> {
|
||||
class Composer.PageStateTest : Components.WebViewTestCase<Composer.WebView> {
|
||||
|
||||
public const string COMPLETE_BODY_TEMPLATE =
|
||||
"""<div id="geary-body" dir="auto">%s<div><br></div><div><br></div></div><div id="geary-signature" dir="auto"></div>""";
|
||||
public const string DIRTY_BODY_TEMPLATE =
|
||||
"""
|
||||
<div id="geary-body" dir="auto" class="geary-focus" contenteditable="true">%s<div><br></div><div><br></div></div>
|
||||
<div id="geary-body" dir="auto" contenteditable="true">%s<div><br></div><div><br></div></div>
|
||||
<div id="geary-signature" class="geary-no-display" dir="auto" contenteditable="true"></div>
|
||||
""";
|
||||
public const string CLEAN_BODY_TEMPLATE = """<div id="geary-body" dir="auto">%s<div><br></div><div><br></div></div>""";
|
||||
|
|
@ -227,7 +227,7 @@ some text
|
|||
}
|
||||
}
|
||||
|
||||
public void clean_content() throws Error {
|
||||
public void clean_content() throws GLib.Error {
|
||||
// XXX split these up into multiple tests
|
||||
load_body_fixture("""
|
||||
http://example1.com
|
||||
|
|
@ -257,20 +257,12 @@ unknown://example6.com
|
|||
I can send email through smtp.gmail.com:587 or through <a href="https://www.gmail.com/">https://www.gmail.com/</a>
|
||||
""";
|
||||
|
||||
try {
|
||||
run_javascript("geary.cleanContent();");
|
||||
string result = Util.JS.to_string(
|
||||
run_javascript("window.document.body.innerHTML;")
|
||||
.get_js_value()
|
||||
);
|
||||
assert(result == DIRTY_BODY_TEMPLATE.printf(expected));
|
||||
} catch (Util.JS.Error err) {
|
||||
print("Util.JS.Error: %s\n", err.message);
|
||||
assert_not_reached();
|
||||
} catch (Error err) {
|
||||
print("WKError: %s\n", err.message);
|
||||
assert_not_reached();
|
||||
}
|
||||
assert_equal(result, DIRTY_BODY_TEMPLATE.printf(expected));
|
||||
}
|
||||
|
||||
public void get_html() throws Error {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
class ConversationPageStateTest : ClientWebViewTestCase<ConversationWebView> {
|
||||
class ConversationPageStateTest : Components.WebViewTestCase<ConversationWebView> {
|
||||
|
||||
public ConversationPageStateTest() {
|
||||
base("ConversationPageStateTest");
|
||||
|
|
|
|||
|
|
@ -82,9 +82,9 @@ test_client_sources = [
|
|||
'client/application/application-certificate-manager-test.vala',
|
||||
'client/application/application-client-test.vala',
|
||||
'client/application/application-configuration-test.vala',
|
||||
'client/components/client-web-view-test.vala',
|
||||
'client/components/client-web-view-test-case.vala',
|
||||
'client/components/components-validator-test.vala',
|
||||
'client/components/components-web-view-test-case.vala',
|
||||
'client/components/components-web-view-test.vala',
|
||||
'client/composer/composer-web-view-test.vala',
|
||||
'client/composer/composer-widget-test.vala',
|
||||
'client/util/util-avatar-test.vala',
|
||||
|
|
@ -92,7 +92,7 @@ test_client_sources = [
|
|||
'client/util/util-email-test.vala',
|
||||
'client/util/util-js-test.vala',
|
||||
|
||||
'js/client-page-state-test.vala',
|
||||
'js/components-page-state-test.vala',
|
||||
'js/composer-page-state-test.vala',
|
||||
'js/conversation-page-state-test.vala',
|
||||
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ int main(string[] args) {
|
|||
client.add_suite(new Application.CertificateManagerTest().suite);
|
||||
client.add_suite(new Application.ClientTest().suite);
|
||||
client.add_suite(new Application.ConfigurationTest().suite);
|
||||
client.add_suite(new ClientWebViewTest().suite);
|
||||
client.add_suite(new Components.WebViewTest().suite);
|
||||
client.add_suite(new Components.ValidatorTest().suite);
|
||||
client.add_suite(new Composer.WebViewTest().suite);
|
||||
client.add_suite(new Composer.WidgetTest().suite);
|
||||
client.add_suite(new Components.ValidatorTest().suite);
|
||||
client.add_suite(new Util.Avatar.Test().suite);
|
||||
client.add_suite(new Util.Cache.Test().suite);
|
||||
client.add_suite(new Util.Email.Test().suite);
|
||||
|
|
@ -64,7 +64,7 @@ int main(string[] args) {
|
|||
|
||||
TestSuite js = new TestSuite("js");
|
||||
|
||||
js.add_suite(new ClientPageStateTest().suite);
|
||||
js.add_suite(new Components.PageStateTest().suite);
|
||||
js.add_suite(new Composer.PageStateTest().suite);
|
||||
js.add_suite(new ConversationPageStateTest().suite);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enables remote image loading in a client web view.
|
||||
*/
|
||||
geary.allowRemoteImages = true;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
/*
|
||||
* Copyright 2016 Michael Gratton <mike@vee.net>
|
||||
* Copyright © 2016-2020 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 ClientWebView and subclasses.
|
||||
* Application logic for Components.WebView and subclasses.
|
||||
*/
|
||||
|
||||
let PageState = function() {
|
||||
|
|
@ -14,13 +14,18 @@ let PageState = function() {
|
|||
};
|
||||
PageState.prototype = {
|
||||
init: function() {
|
||||
this.allowRemoteImages = false;
|
||||
this.isLoaded = false;
|
||||
this.undoEnabled = false;
|
||||
this.redoEnabled = false;
|
||||
this.hasSelection = false;
|
||||
this.lastPreferredHeight = 0;
|
||||
|
||||
this._selectionChanged = MessageSender("selection_changed");
|
||||
this._contentLoaded = MessageSender("content_loaded");
|
||||
this._preferredHeightChanged = MessageSender("preferred_height_changed");
|
||||
this._commandStackChanged = MessageSender("command_stack_changed");
|
||||
this._documentModified = MessageSender("document_modified");
|
||||
|
||||
let state = this;
|
||||
|
||||
// Set up an observer to keep track of modifications made to
|
||||
|
|
@ -40,6 +45,10 @@ PageState.prototype = {
|
|||
state.loaded();
|
||||
});
|
||||
|
||||
document.addEventListener("selectionchange", function(e) {
|
||||
state.selectionChanged();
|
||||
});
|
||||
|
||||
// Coalesce multiple calls to updatePreferredHeight using a
|
||||
// timeout to avoid the overhead of multiple JS messages sent
|
||||
// to the app and hence view multiple resizes being queued.
|
||||
|
|
@ -88,6 +97,8 @@ PageState.prototype = {
|
|||
window.addEventListener("transitionend", function(e) {
|
||||
queuePreferredHeightUpdate();
|
||||
}, false); // load does not bubble
|
||||
|
||||
this.testResult = null;
|
||||
},
|
||||
getPreferredHeight: function() {
|
||||
// Return the scroll height of the HTML element since the BODY
|
||||
|
|
@ -105,10 +116,10 @@ PageState.prototype = {
|
|||
// be vaguegly correct when notifying of the HTML load
|
||||
// completing.
|
||||
this.updatePreferredHeight();
|
||||
window.webkit.messageHandlers.contentLoaded.postMessage(null);
|
||||
this._contentLoaded();
|
||||
},
|
||||
loadRemoteImages: function() {
|
||||
this.allowRemoteImages = true;
|
||||
window._gearyAllowRemoteResourceLoads = true;
|
||||
let images = document.getElementsByTagName("IMG");
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
let img = images.item(i);
|
||||
|
|
@ -140,9 +151,6 @@ PageState.prototype = {
|
|||
stopBodyObserver: function() {
|
||||
this.bodyObserver.disconnect();
|
||||
},
|
||||
remoteImageLoadBlocked: function() {
|
||||
window.webkit.messageHandlers.remoteImageLoadBlocked.postMessage(null);
|
||||
},
|
||||
/**
|
||||
* Sends "preferredHeightChanged" message if it has changed.
|
||||
*/
|
||||
|
|
@ -159,9 +167,7 @@ PageState.prototype = {
|
|||
// shrink again, leading to visual flicker.
|
||||
if (this.isLoaded && height > 0 && height != this.lastPreferredHeight) {
|
||||
this.lastPreferredHeight = height;
|
||||
window.webkit.messageHandlers.preferredHeightChanged.postMessage(
|
||||
height
|
||||
);
|
||||
this._preferredHeightChanged(height);
|
||||
}
|
||||
},
|
||||
checkCommandStack: function() {
|
||||
|
|
@ -171,19 +177,38 @@ PageState.prototype = {
|
|||
if (canUndo != this.undoEnabled || canRedo != this.redoEnabled) {
|
||||
this.undoEnabled = canUndo;
|
||||
this.redoEnabled = canRedo;
|
||||
window.webkit.messageHandlers.commandStackChanged.postMessage(
|
||||
this.undoEnabled + "," + this.redoEnabled
|
||||
);
|
||||
this._commandStackChanged(this.undoEnabled, this.redoEnabled);
|
||||
}
|
||||
},
|
||||
documentModified: function(element) {
|
||||
window.webkit.messageHandlers.documentModified.postMessage(null);
|
||||
this._documentModified();
|
||||
},
|
||||
selectionChanged: function() {
|
||||
let hasSelection = !window.getSelection().isCollapsed;
|
||||
if (this.hasSelection != hasSelection) {
|
||||
this.hasSelection = hasSelection;
|
||||
window.webkit.messageHandlers.selectionChanged.postMessage(hasSelection);
|
||||
this._selectionChanged(hasSelection);
|
||||
}
|
||||
},
|
||||
// Methods below are for unit tests.
|
||||
testVoid: function() {
|
||||
this.testResult = "void";
|
||||
},
|
||||
testReturn: function(value) {
|
||||
this.testResult = value;
|
||||
return value;
|
||||
},
|
||||
testThrow: function(value) {
|
||||
this.testResult = value;
|
||||
throw this.testResult;
|
||||
}
|
||||
};
|
||||
|
||||
let MessageSender = function(name) {
|
||||
return function() {
|
||||
// Since typeof(arguments) == 'object', convert to an array so
|
||||
// that Components.WebView.MessageCallback callbacks get
|
||||
// arrays or tuples rather than dicts as arguments
|
||||
_GearyWebExtension.send(name, Array.from(arguments));
|
||||
};
|
||||
};
|
||||
|
|
@ -43,12 +43,12 @@ body > div#geary-quote {
|
|||
padding: 6px !important;
|
||||
}
|
||||
|
||||
body > div.geary-focus {
|
||||
body > div:focus-within {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body > div#geary-signature.geary-focus,
|
||||
body > div#geary-quote.geary-focus {
|
||||
body > div#geary-signature:focus-within,
|
||||
body > div#geary-quote:focus-within {
|
||||
outline: 1px dashed #ccc !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright 2016 Software Freedom Conservancy Inc.
|
||||
* Copyright 2016 Michael Gratton <mike@vee.net>
|
||||
* Copyright © 2016 Software Freedom Conservancy Inc.
|
||||
* Copyright © 2016-2020 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.
|
||||
|
|
@ -35,6 +35,9 @@ ComposerPageState.prototype = {
|
|||
this.nextSelectionId = 0;
|
||||
this.cursorContext = null;
|
||||
|
||||
this._cursorContextChanged = MessageSender("cursor_context_changed");
|
||||
this._dragDropReceived = MessageSender("drag_drop_received");
|
||||
|
||||
document.addEventListener("click", function(e) {
|
||||
if (e.target.tagName == "A") {
|
||||
e.preventDefault();
|
||||
|
|
@ -99,7 +102,9 @@ ComposerPageState.prototype = {
|
|||
}, true);
|
||||
|
||||
// Handle file drag & drop
|
||||
document.body.addEventListener("drop", state.handleFileDrop, true);
|
||||
document.body.addEventListener("drop", function(e) {
|
||||
state.handleFileDrop(e);
|
||||
}, true);
|
||||
document.body.addEventListener("allowDrop", function(e) {
|
||||
ev.preventDefault();
|
||||
}, true);
|
||||
|
|
@ -118,7 +123,6 @@ ComposerPageState.prototype = {
|
|||
|
||||
// Focus within the HTML document
|
||||
document.body.focus();
|
||||
this.updateFocusClass(this.bodyPart);
|
||||
|
||||
// Set text cursor at appropriate position
|
||||
let cursor = document.getElementById("cursormarker");
|
||||
|
|
@ -346,35 +350,9 @@ ComposerPageState.prototype = {
|
|||
let newContext = new EditContext(cursor);
|
||||
if (!newContext.equals(this.cursorContext)) {
|
||||
this.cursorContext = newContext;
|
||||
window.webkit.messageHandlers.cursorContextChanged.postMessage(
|
||||
newContext.encode()
|
||||
);
|
||||
this._cursorContextChanged(newContext.encode());
|
||||
}
|
||||
}
|
||||
|
||||
while (cursor != null) {
|
||||
let parent = cursor.parentNode;
|
||||
if (parent == document.body) {
|
||||
this.updateFocusClass(cursor);
|
||||
break;
|
||||
}
|
||||
cursor = parent;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Work around WebKit note yet supporting :focus-inside pseudoclass.
|
||||
*/
|
||||
updateFocusClass: function(newFocus) {
|
||||
if (this.focusedPart != null) {
|
||||
this.focusedPart.classList.remove("geary-focus");
|
||||
this.focusedPart = null;
|
||||
}
|
||||
if (newFocus == this.bodyPart ||
|
||||
newFocus == this.signaturePart ||
|
||||
newFocus == this.quotePart) {
|
||||
this.focusedPart = newFocus;
|
||||
this.focusedPart.classList.add("geary-focus");
|
||||
}
|
||||
},
|
||||
containedInPart: function(target) {
|
||||
let inPart = false;
|
||||
|
|
@ -396,13 +374,14 @@ ComposerPageState.prototype = {
|
|||
continue;
|
||||
|
||||
const reader = new FileReader();
|
||||
const state = this;
|
||||
reader.onload = (function(filename, imageType) { return function(loadEvent) {
|
||||
// Remove prefixed file type and encoding type
|
||||
var parts = loadEvent.target.result.split(",");
|
||||
if (parts.length < 2)
|
||||
return;
|
||||
|
||||
window.webkit.messageHandlers.dragDropReceived.postMessage({
|
||||
state._dragDropReceived({
|
||||
fileName: encodeURIComponent(filename),
|
||||
fileType: imageType,
|
||||
content: parts[1]
|
||||
|
|
|
|||
|
|
@ -473,6 +473,7 @@
|
|||
<object class="GtkRevealer" id="body_revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="transition_type">slide-up</property>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ ConversationPageState.prototype = {
|
|||
init: function() {
|
||||
PageState.prototype.init.apply(this, []);
|
||||
|
||||
this._deceptiveLinkClicked = MessageSender("deceptive_link_clicked");
|
||||
|
||||
let state = this;
|
||||
document.addEventListener("click", function(e) {
|
||||
if (e.target.tagName == "A" &&
|
||||
|
|
@ -267,7 +269,7 @@ ConversationPageState.prototype = {
|
|||
let reason = ConversationPageState.isDeceptiveText(text, href);
|
||||
if (reason != ConversationPageState.NOT_DECEPTIVE) {
|
||||
cancelClick = true;
|
||||
window.webkit.messageHandlers.deceptiveLinkClicked.postMessage({
|
||||
this._deceptiveLinkClicked({
|
||||
reason: reason,
|
||||
text: text,
|
||||
href: href,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@
|
|||
<file compressed="true" preprocess="xml-stripblanks">accounts_editor_servers_pane.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">application-main-window.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">certificate_warning_dialog.glade</file>
|
||||
<file compressed="true">client-web-view.js</file>
|
||||
<file compressed="true">client-web-view-allow-remote-images.js</file>
|
||||
<file compressed="true">components-web-view.js</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">components-attachment-pane.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">components-attachment-pane-menus.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">components-attachment-view.ui</file>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue