From 6ff70f1a9ae70e3481411d27de7594477fa4d0d2 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Fri, 15 May 2015 23:04:42 -0700 Subject: WebHelper2 for translating WebKit2 views This adds a new Javascript module, WebHelper2. It's the WebKit2 analogue to WebHelper. It offers a facility for calling gettext() on the contents of DOM elements in your web page. It accomplishes this using an extension module that's loaded into WebKit's web process. [endlessm/eos-sdk#291] --- .gitignore | 1 + Makefile.am | 35 +++- configure.ac | 6 + jasmine.json | 5 + test/Makefile.am.inc | 2 + test/smoke-tests/webhelper/webview2.js | 83 ++++++++ test/webhelper/testTranslate2.js | 119 ++++++++++++ webhelper/webextensions/wh2extension.c | 298 ++++++++++++++++++++++++++++ webhelper/webhelper2.js | 322 +++++++++++++++++++++++++++++++ webhelper/webhelper_private/config.js.in | 1 + 10 files changed, 871 insertions(+), 1 deletion(-) create mode 100644 test/smoke-tests/webhelper/webview2.js create mode 100644 test/webhelper/testTranslate2.js create mode 100644 webhelper/webextensions/wh2extension.c create mode 100644 webhelper/webhelper2.js create mode 100644 webhelper/webhelper_private/config.js.in diff --git a/.gitignore b/.gitignore index 79a0993..bffe54c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ endless/eosresource.c endless/eosresource-private.h tools/eos-application-manifest/eos-application-manifest tools/eos-json-extractor/eos-json-extractor +webhelper/webhelper_private/config.js *.py[cod] diff --git a/Makefile.am b/Makefile.am index 575d6f2..5dc8696 100644 --- a/Makefile.am +++ b/Makefile.am @@ -46,6 +46,27 @@ dist-hook:: @false endif +# # # SUBSTITUTED FILES # # # +# These files need to be filled in with make variables + +subst = $(SED) \ + -e 's,%libexecdir%,$(libexecdir),g' \ + $(NULL) + +subst_files = \ + webhelper/webhelper_private/config.js \ + $(NULL) + +$(subst_files): %: %.in Makefile + $(AM_V_GEN)$(MKDIR_P) $(@D) && \ + rm -f $@ $@.tmp && \ + $(subst) $< > $@.tmp && \ + chmod a-w $@.tmp && \ + mv $@.tmp $@ + +CLEANFILES += $(subst_files) +EXTRA_DIST += $(patsubst %,%.in,$(subst_files)) + # # # LIBRARY # # # # Main Open Endless SDK library @@ -64,16 +85,28 @@ DISTCLEANFILES += @EOS_SDK_API_NAME@.pc # # # WEBHELPER LIBRARY # # # -webhelper_sources = webhelper/webhelper.js +webhelper_sources = \ + webhelper/webhelper.js \ + webhelper/webhelper2.js \ + $(NULL) gjsmodulesdir = $(datadir)/gjs-1.0 webhelperdir = $(gjsmodulesdir) +webhelper_privatedir = $(webhelperdir)/webhelper_private dist_webhelper_DATA = \ $(webhelper_sources) \ $(NULL) +dist_webhelper_private_DATA = webhelper/webhelper_private/config.js EOS_JS_COVERAGE_FILES = $(webhelper_sources) +webhelper2extensionsdir = $(libexecdir)/webhelper2 +webhelper2extensions_LTLIBRARIES = wh2extension.la +wh2extension_la_SOURCES = webhelper/webextensions/wh2extension.c +wh2extension_la_CPPFLAGS = @WEBHELPER2_EXTENSION_CFLAGS@ +wh2extension_la_LIBADD = @WEBHELPER2_EXTENSION_LIBS@ +wh2extension_la_LDFLAGS = -module -avoid-version -no-undefined + # # # INTROSPECTION FILES # # # -include $(INTROSPECTION_MAKEFILE) diff --git a/configure.ac b/configure.ac index 2f1d344..fe37a8b 100644 --- a/configure.ac +++ b/configure.ac @@ -83,6 +83,7 @@ GOBJECT_REQUIREMENT="gobject-2.0" GIO_REQUIREMENT="gio-2.0" GTK_REQUIREMENT="gtk+-3.0 >= 3.10" JSON_GLIB_REQUIREMENT="json-glib-1.0 >= 0.12" +WEBKIT2_REQUIREMENT="webkit2gtk-4.0" # These go into the pkg-config file as Requires: and Requires.private: # (Generally, use Requires.private: instead of Requires:) EOS_REQUIRED_MODULES= @@ -216,10 +217,15 @@ AC_SUBST([JASMINE_REPORT_ARGUMENT]) PKG_CHECK_MODULES([EOS_SDK], [ $EOS_REQUIRED_MODULES $EOS_REQUIRED_MODULES_PRIVATE]) +PKG_CHECK_MODULES([WEBHELPER2_EXTENSION], [ + $GLIB_REQUIREMENT + $GOBJECT_REQUIREMENT + $WEBKIT2_REQUIREMENT]) # Check installed GIRs for webhelper JS module EOS_CHECK_GJS_GIR([GLib], [2.0]) EOS_CHECK_GJS_GIR([WebKit], [3.0]) +EOS_CHECK_GJS_GIR([WebKit2], [4.0]) # Code coverage reports support EOS_COVERAGE_REPORT([c js]) diff --git a/jasmine.json b/jasmine.json index ff37382..4d2ef8b 100644 --- a/jasmine.json +++ b/jasmine.json @@ -6,6 +6,11 @@ "test/webhelper", "test/tools/eos-application-manifest" ], + "exclude": [ + "test/webhelper/testTranslate.js", + "test/webhelper/testUpdateFontSize.js", + "test/webhelper/testWebActions.js" + ], "environment": { "GI_TYPELIB_PATH": ".", "LD_LIBRARY_PATH": ".libs", diff --git a/test/Makefile.am.inc b/test/Makefile.am.inc index 11e4dea..7e10953 100644 --- a/test/Makefile.am.inc +++ b/test/Makefile.am.inc @@ -44,6 +44,7 @@ EXTRA_DIST += \ javascript_tests = \ test/tools/eos-application-manifest/testInit.js \ test/webhelper/testTranslate.js \ + test/webhelper/testTranslate2.js \ test/webhelper/testWebActions.js \ test/webhelper/testUpdateFontSize.js \ test/endless/testCustomContainer.js \ @@ -78,4 +79,5 @@ TESTS_ENVIRONMENT = \ export GI_TYPELIB_PATH="$(top_builddir)$${GI_TYPELIB_PATH:+:$$GI_TYPELIB_PATH}"; \ export LD_LIBRARY_PATH="$(top_builddir)/.libs$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH}"; \ export XDG_CONFIG_HOME=`mktemp -d $${TMPDIR:-/tmp}/sdktestconfig.XXXXXXXX`; \ + export WEBHELPER_UNINSTALLED_EXTENSION_DIR="$(top_builddir)/.libs"; \ $(NULL) diff --git a/test/smoke-tests/webhelper/webview2.js b/test/smoke-tests/webhelper/webview2.js new file mode 100644 index 0000000..604819c --- /dev/null +++ b/test/smoke-tests/webhelper/webview2.js @@ -0,0 +1,83 @@ +// Copyright 2015 Endless Mobile, Inc. + +const Endless = imports.gi.Endless; +const Gettext = imports.gettext; +const Lang = imports.lang; +const WebHelper2 = imports.webhelper2; +const WebKit2 = imports.gi.WebKit2; + +const TEST_APPLICATION_ID = 'com.endlessm.example.test-webview2'; +const TEST_HTML = '\ + \ + \ +First page \ + \ + \ +\ + \ +

First page

\ +\ +

Regular link to a Web site

\ +\ +

This is text that will be italicized: Hello, \ +world!

\ +\ + \ +'; + +const TestApplication = new Lang.Class({ + Name: 'TestApplication', + Extends: Endless.Application, + + vfunc_dbus_register: function (connection, object_path) { + this._webhelper = new WebHelper2.WebHelper({ + well_known_name: this.application_id, + connection: connection, + }); + return this.parent(connection, object_path); + }, + + vfunc_startup: function () { + this.parent(); + + this._webhelper.set_gettext((s) => s.italics()); + + this._webview = new WebKit2.WebView(); + this._webview.connect('load-changed', (webview, event) => { + if (event === WebKit2.LoadEvent.FINISHED) + this._webhelper.translate_html(webview, null, (obj, res) => { + this._webhelper.translate_html_finish(res); + }); + }); + this._webview.load_html(TEST_HTML, 'file://'); + + this._window = new Endless.Window({ + application: this, + border_width: 16, + }); + + this._pm = this._window.page_manager; + this._pm.add(this._webview, { name: 'page1' }); + + this._window.show_all(); + }, + + vfunc_dbus_unregister: function (connection, object_path) { + this.parent(connection, object_path); + this._webhelper.unregister(); + }, +}); + +let app = new TestApplication({ + application_id: TEST_APPLICATION_ID, +}); +app.run(ARGV); diff --git a/test/webhelper/testTranslate2.js b/test/webhelper/testTranslate2.js new file mode 100644 index 0000000..67f7b1f --- /dev/null +++ b/test/webhelper/testTranslate2.js @@ -0,0 +1,119 @@ +// Copyright 2015 Endless Mobile, Inc. + +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const WebHelper2 = imports.webhelper2; +const WebKit2 = imports.gi.WebKit2; + +const WELL_KNOWN_NAME = 'com.endlessm.WebHelper.testTranslate2'; + +Gtk.init(null); + +describe('WebHelper2 translator', function () { + let webhelper, owner_id, connection; + + beforeAll(function (done) { + owner_id = Gio.DBus.own_name(Gio.BusType.SESSION, WELL_KNOWN_NAME, + Gio.BusNameOwnerFlags.NONE, + null, // bus acquired + (con, name) => { // name acquired + connection = con; + done(); + }, + null); // name lost + }); + + afterAll(function () { + Gio.DBus.unown_name(owner_id); + }); + + beforeEach(function () { + webhelper = new WebHelper2.WebHelper({ + well_known_name: WELL_KNOWN_NAME, + connection: connection, + }); + }); + + afterEach(function () { + webhelper.unregister(); + }); + + it('complains about a bad gettext function', function () { + expect(function () { + webhelper.set_gettext('I am not a function'); + }).toThrow(); + }); + + it('gets and sets the gettext function', function () { + let translation_function = (s) => s; + webhelper.set_gettext(translation_function); + expect(webhelper.get_gettext()).toBe(translation_function); + }); + + it('has a null gettext function by default', function () { + expect(webhelper.get_gettext()).toBeNull(); + }); + + it('can remove the gettext function by setting null', function () { + webhelper.set_gettext((s) => s); + expect(webhelper.get_gettext()).not.toBeNull(); + webhelper.set_gettext(null); + expect(webhelper.get_gettext()).toBeNull(); + }); + + describe('translating a page', function () { + let webview; + + function run_loop() { + webview.connect('load-changed', (webview, event) => { + if (event === WebKit2.LoadEvent.FINISHED) { + webhelper.translate_html(webview, null, (obj, res) => { + webhelper.translate_html_finish(res); + Mainloop.quit('webhelper2'); + }); + } + }); + webview.load_html('

Translate Me

', + null); + Mainloop.run('webhelper2'); + } + + beforeEach(function () { + webview = new WebKit2.WebView(); + }); + + it('translates a string', function () { + let gettext_spy = jasmine.createSpy('gettext_spy').and.callFake((s) => s); + webhelper.set_gettext(gettext_spy); + run_loop(); + expect(gettext_spy).toHaveBeenCalledWith('Translate Me'); + }); + + // The following test is disabled because GJS cannot catch exceptions + // across FFI interfaces (e.g. in GObject callbacks.) + xit('complains about a gettext function not being set', function () { + expect(function () { + run_loop(); + }).toThrow(); + }); + + it('can cancel the translation operation', function (done) { + webhelper.set_gettext((s) => s); + webview.connect('load-changed', (webview, event) => { + if (event === WebKit2.LoadEvent.FINISHED) { + let cancellable = new Gio.Cancellable(); + cancellable.cancel(); + webhelper.translate_html(webview, cancellable, (obj, res) => { + expect(function () { + webhelper.translate_html_finish(res); + }).toThrow(); + done(); + }); + } + }); + webview.load_html('', null); + }); + }); +}); diff --git a/webhelper/webextensions/wh2extension.c b/webhelper/webextensions/wh2extension.c new file mode 100644 index 0000000..c51171e --- /dev/null +++ b/webhelper/webextensions/wh2extension.c @@ -0,0 +1,298 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* Copyright 2015 Endless Mobile, Inc. */ + +#include + +#include +#include +#include + +#define WH2_DBUS_INTERFACE_NAME "com.endlessm.WebHelper2.Translation" +#define MAIN_PROGRAM_OBJECT_PATH "/com/endlessm/gettext" +#define MAIN_PROGRAM_INTERFACE_NAME "com.endlessm.WebHelper2.Gettext" + +/* Declaration of externally visible symbol */ +void webkit_web_extension_initialize_with_user_data (WebKitWebExtension *, const GVariant *); + +typedef struct { + GDBusConnection *connection; /* unowned */ + GDBusNodeInfo *node; /* owned */ + GDBusInterfaceInfo *interface; /* owned by node */ + GSList *bus_ids; /* GSList; owned */ + GSList *page_ctxts; /* GSList; owned */ + gchar *main_program_name; /* owned; well-known-name of main program */ +} Context; + +typedef struct { + Context *ctxt; /* unowned */ + WebKitWebPage *page; /* unowned */ +} PageContext; + +static const gchar introspection_xml[] = + "" + "" + "" + "" + ""; + +static void +context_free (Context *ctxt) +{ + g_clear_pointer (&ctxt->node, g_dbus_node_info_unref); + g_clear_pointer (&ctxt->bus_ids, g_slist_free); + g_clear_pointer (&ctxt->main_program_name, g_free); + g_slist_free_full (ctxt->page_ctxts, (GDestroyNotify) g_free); + ctxt->page_ctxts = NULL; + g_free (ctxt); +} + +static gchar * +translation_function (const gchar *message, + Context *ctxt) +{ + GError *error = NULL; + GVariant *result = + g_dbus_connection_call_sync (ctxt->connection, ctxt->main_program_name, + MAIN_PROGRAM_OBJECT_PATH, + MAIN_PROGRAM_INTERFACE_NAME, "Gettext", + g_variant_new ("(s)", message), + (GVariantType *) "(s)", + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1 /* timeout */, NULL /* cancellable */, + &error); + if (result == NULL) + { + g_warning ("No return value from gettext: %s", error->message); + g_clear_error (&error); + return g_strdup (message); + } + + gchar *retval; + g_variant_get (result, "(s)", &retval); + g_variant_unref (result); + return retval; +} + +static void +translate_html (WebKitDOMDocument *dom, + Context *ctxt) +{ + WebKitDOMNodeList *translatable; + GError *error = NULL; + + translatable = webkit_dom_document_get_elements_by_name (dom, "translatable"); + + gulong index, length = webkit_dom_node_list_get_length (translatable); + for (index = 0; index < length; index++) + { + WebKitDOMNode *element = webkit_dom_node_list_item (translatable, index); + + /* Translate the text */ + if (WEBKIT_DOM_IS_HTML_ELEMENT (element)) + { + WebKitDOMHTMLElement *el_html = WEBKIT_DOM_HTML_ELEMENT (element); + gchar *inner_html = webkit_dom_html_element_get_inner_html (el_html); + gchar *translated_html = translation_function (inner_html, ctxt); + webkit_dom_html_element_set_inner_html (el_html, translated_html, + &error); + if (error != NULL) + { + g_warning ("There was a problem translating '%s' to '%s': %s", + inner_html, translated_html, error->message); + g_clear_error (&error); + } + + g_free (translated_html); + g_free (inner_html); + } + else + { + gchar *text = webkit_dom_node_get_text_content (element); + gchar *translated_text = translation_function (text, ctxt); + webkit_dom_node_set_text_content (element, translated_text, &error); + if (error != NULL) + { + g_warning ("There was a problem translating '%s' to '%s': %s", + text, translated_text, error->message); + g_clear_error (&error); + } + + g_free (translated_text); + g_free (text); + } + } + + g_object_unref (translatable); +} + +static void +on_wh2_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + PageContext *pctxt) +{ + if (strcmp (method_name, "Translate") != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method %s invoked on interface %s", + method_name, interface_name); + return; + } + + WebKitDOMDocument *document = webkit_web_page_get_dom_document (pctxt->page); + if (document == NULL) + { + g_dbus_method_invocation_return_error_literal (invocation, G_IO_ERROR, + G_IO_ERROR_NOT_INITIALIZED, + "The web page has not loaded a document yet"); + return; + } + + translate_html (document, pctxt->ctxt); + + g_dbus_method_invocation_return_value (invocation, NULL); +} + +static GDBusInterfaceVTable dbus_impl_vtable = { + (GDBusInterfaceMethodCallFunc) on_wh2_method_call, + NULL, /* get property */ + NULL /* set property */ +}; + +static gboolean +register_object (PageContext *pctxt) +{ + GError *error = NULL; + + if (pctxt->ctxt->connection == NULL) + return G_SOURCE_CONTINUE; /* Try again when the connection is ready */ + + /* The ID is known to the main process and the web process. So we can address + a specific web page over DBus. */ + guint64 id = webkit_web_page_get_id (pctxt->page); + + gchar *object_path = g_strdup_printf("/com/endlessm/webview/%" + G_GUINT64_FORMAT, id); + + guint bus_id = g_dbus_connection_register_object (pctxt->ctxt->connection, + object_path, + pctxt->ctxt->interface, + &dbus_impl_vtable, + pctxt, NULL, + &error); + if (bus_id == 0) + { + g_critical ("Failed to export webview object on bus: %s", error->message); + g_clear_error (&error); + goto out; + } + + pctxt->ctxt->bus_ids = g_slist_prepend (pctxt->ctxt->bus_ids, + GUINT_TO_POINTER (bus_id)); + +out: + g_free (object_path); + return G_SOURCE_REMOVE; +} + +static void +on_page_created (WebKitWebExtension *extension, + WebKitWebPage *page, + Context *ctxt) +{ + PageContext *pctxt = g_new0 (PageContext, 1); + pctxt->ctxt = ctxt; + pctxt->page = page; + + ctxt->page_ctxts = g_slist_prepend (ctxt->page_ctxts, pctxt); + + g_idle_add_full (G_PRIORITY_HIGH_IDLE, (GSourceFunc) register_object, + pctxt, NULL); +} + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + Context *ctxt) +{ + GError *error = NULL; + + ctxt->connection = connection; + + /* Export our interface on the bus */ + ctxt->node = g_dbus_node_info_new_for_xml (introspection_xml, &error); + if (ctxt->node == NULL) + goto fail; + ctxt->interface = g_dbus_node_info_lookup_interface (ctxt->node, + WH2_DBUS_INTERFACE_NAME); + if (ctxt->interface == NULL) + goto fail; + + return; + +fail: + if (error != NULL) + { + g_critical ("Error hooking up web extension DBus interface: %s", + error->message); + g_clear_error (&error); + } + else + { + g_critical ("Unknown error hooking up web extension DBus interface"); + } +} + +static void +unregister_object (gpointer data, + GDBusConnection *connection) +{ + guint bus_id = GPOINTER_TO_UINT (data); + if (!g_dbus_connection_unregister_object (connection, bus_id)) + g_critical ("Trouble unregistering object"); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + Context *ctxt) +{ + if (connection == NULL) + { + g_warning ("Could not initialize DBus interface for WebHelper2 " + "extension; the name %s was lost.", name); + return; + } + + g_slist_foreach (ctxt->bus_ids, (GFunc) unregister_object, connection); +} + +/* Receives the main program's unique DBus name as user data. */ +G_MODULE_EXPORT void +webkit_web_extension_initialize_with_user_data (WebKitWebExtension *extension, + const GVariant *data_from_app) +{ + const gchar *name = g_variant_get_string ((GVariant *) data_from_app, NULL); + + Context *ctxt = g_new0 (Context, 1); + ctxt->main_program_name = g_strdup (name); + gchar *well_known_name = g_strconcat (name, ".webhelper", NULL); + + g_signal_connect (extension, "page-created", + G_CALLBACK (on_page_created), ctxt); + + g_bus_own_name (G_BUS_TYPE_SESSION, well_known_name, + G_BUS_NAME_OWNER_FLAGS_NONE, + (GBusAcquiredCallback) on_bus_acquired, + NULL, /* name acquired callback */ + (GBusNameLostCallback) on_name_lost, + ctxt, (GDestroyNotify) context_free); + + g_free (well_known_name); +} diff --git a/webhelper/webhelper2.js b/webhelper/webhelper2.js new file mode 100644 index 0000000..beb0333 --- /dev/null +++ b/webhelper/webhelper2.js @@ -0,0 +1,322 @@ +// Copyright 2015 Endless Mobile, Inc. + +imports.gi.versions.WebKit2 = '4.0'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Lang = imports.lang; +const WebKit2 = imports.gi.WebKit2; + +const Config = imports.webhelper_private.config; + +const DBUS_WEBVIEW_EXPORT_PATH = '/com/endlessm/webview/'; +const WH2_DBUS_EXTENSION_INTERFACE = '\ + \ + \ + \ + \ + '; +const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\ + \ + \ + \ + \ + \ + \ + \ + '; + +/** + * Namespace: WebHelper2 + * Convenience library for running web applications + * + * WebHelper is a convenience library for displaying web applications inside a + * GTK container running WebKitGTK. + * WebHelper2 is the WebKit2 version. + * + * One often-encountered problem is localizing text through the same API as + * your main GTK program. + * WebHelper solves this problem by allowing you to mark strings in your HTML + * page and translating them through a function of your choice when you run + * . + */ + +/** + * Class: WebHelper + * Helper object for a WebKit2 web application + * + * Constructor parameters: + * props - a dictionary of construction properties and values (default {}) + * + * The application class for your web application should create in + * its *vfunc_dbus_register()* implementation, with appropriate + * and parameters. + * After that, you can do further setup on it, such as defining web actions, in + * your *vfunc_startup()* implementation. + * + * There is no need to set up specially any web views that you create, unlike + * WebKit1 where you must set up . + * + * Example: + * > const TestApplication = new Lang.Class({ + * > Name: 'TestApplication', + * > Extends: Gtk.Application, + * > + * > vfunc_dbus_register: function (connection, object_path) { + * > this._webhelper = new WebHelper2.WebHelper({ + * > well_known_name: this.application_id, + * > connection: connection, + * > }); + * > return this.parent(connection, object_path); + * > }, + * > + * > vfunc_startup: function () { + * > this.parent(); + * > + * > this._webhelper.set_gettext(Gettext.dgettext.bind(null, + * > GETTEXT_DOMAIN)); + * > + * > let window = new Gtk.Window(); + * > let webview = new WebKit2.WebView(); + * > webview.connect('load-changed', (webview, event) => { + * > if (event === WebKit2.LoadEvent.FINISHED) + * > this._webhelper.translate_html(webview, null, (obj, res) => { + * > this._webhelper.translate_html_finish(res); + * > window.show_all(); + * > }); + * > }); + * > window.add(webview); + * > webview.load_uri('file:///path/to/my/page.html'); + * > }, + * > + * > vfunc_dbus_unregister: function (connection, object_path) { + * > this.parent(connection, object_path); + * > this._webhelper.unregister(); + * > }, + * >}); + * > + * >let app = new TestApplication({ + * > application_id: 'com.example.SmokeGrinder', + * >}); + * >app.run(ARGV); + */ +const WebHelper = new Lang.Class({ + Name: 'WebHelper', + GTypeName: 'Wh2WebHelper', + Extends: GObject.Object, + Properties: { + /** + * Property: well-known-name + * Well-known bus name owned by the calling program + * + * Type: + * string + * + * This property is required at construction time. + * It must conform to . + * + * This must be a well-known bus name that your program owns. + * The easiest way to ensure that is to use your application's ID, since + * every application class registers its ID as a bus name. + */ + 'well-known-name': GObject.ParamSpec.string('well-known-name', + 'Well-known name', + 'Well-known bus name owned by the calling program', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + ''), + /** + * Property: connection + * DBus connection owned by the calling program + * + * Type: + * *Gio.DBusConnection* + * + * This property is required at construction time. + * + * This must be a DBus connection object that your program owns. + * The easiest way to ensure that is to use the connection object passed + * in to your application's *vfunc_dbus_register()* function. + */ + 'connection': GObject.ParamSpec.object('connection', 'Connection', + 'DBus connection owned by the calling program', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + Gio.DBusConnection.$gtype), + }, + + _init: function (props={}) { + this._gettext = null; + this._ProxyConstructor = + Gio.DBusProxy.makeProxyWrapper(WH2_DBUS_EXTENSION_INTERFACE); + this.parent(props); + + if (this.well_known_name === '') + throw new Error('The "well-known-name" parameter is required.'); + this._extension_name = this.well_known_name + '.webhelper'; + + // Set up Webkit to load our web extension + let context = WebKit2.WebContext.get_default(); + context.connect('initialize-web-extensions', () => { + let libexec = Gio.File.new_for_path(Config.LIBEXECDIR); + let path = libexec.get_child('webhelper2').get_path(); + + let localpath = GLib.getenv('WEBHELPER_UNINSTALLED_EXTENSION_DIR'); + if (localpath) + path = localpath; + + context.set_web_extensions_directory(path); + context.set_web_extensions_initialization_user_data(new GLib.Variant('s', + this.well_known_name)); + }); + + // Export our own DBus interface + this._dbus_impl = + Gio.DBusExportedObject.wrapJSObject(WH2_DBUS_MAIN_PROGRAM_INTERFACE, + this); + this._dbus_impl.export(this.connection, '/com/endlessm/gettext'); + }, + + // DBus implementations + + Gettext: function (string) { + return this._gettext(string); + }, + + // Public API + + /** + * Method: set_gettext + * Define function which translates text + * + * Parameters: + * gettext_func - a function, or null + * + * When you plan to use the function to translate text in + * your web application, set this property to the translation function. + * The function must take one parameter, a string, and also return a + * string. + * The canonical example is gettext(). + * + * Pass null for _gettext_func_ to unset the translation function. + * + * If this function has not been called, or has most recently been called + * with null, then it is illegal to call . + * + * Example: + * > const Gettext = imports.gettext; + * > const GETTEXT_DOMAIN = 'smoke-grinder'; + * > + * > webhelper.set_gettext(Gettext.dgettext.bind(null, GETTEXT_DOMAIN)); + */ + set_gettext: function (gettext_func) { + if (gettext_func !== null && typeof gettext_func !== 'function') + throw new Error('The translation function must be a function, or ' + + 'null.'); + this._gettext = gettext_func; + }, + + /** + * Method: get_gettext + * Retrieve the currently set translation function + * + * Returns: + * the translation function previously set with , or null + * if none is currently set. + */ + get_gettext: function () { + return this._gettext; + }, + + /** + * Method: translate_html + * Translate the HTML page in a webview asynchronously + * + * Parameters: + * webview - a *WebKit2.WebView* with HTML loaded + * cancellable - a *Gio.Cancellable*, or null + * callback - a function that takes two parameters: this + * object, and a result object; or null if you don't want a callback + * + * Perform translation on all HTML elements marked with name="translatable" + * in the HTML document displaying in _webview_. + * The translation will be performed using the function you have set using + * . + * + * When the translation is finished, _callback_ will be called. + * You can get the result of the operation by calling + * with the _result_ object passed to _callback_. + * + * You can optionally pass _cancellable_ if you want to be able to cancel + * the operation. + * + * Example: + * > webview.connect('load-changed', (webview, event) => { + * > if (event === WebKit2.LoadEvent.FINISHED) + * > webhelper.translate_html(webview, null, (obj, res) => { + * > webhelper.translate_html_finish(res); + * > // Translation done, show the page + * > webview.show_all(); + * > }); + * > }); + */ + translate_html: function (webview, cancellable, callback) { + let task = { callback: callback }; + // Wait for the DBus interface to appear on the bus + task.watch_id = Gio.DBus.watch_name(Gio.BusType.SESSION, + this._extension_name, Gio.BusNameWatcherFlags.NONE, + (connection, name, owner) => { + // name appeared + let webview_object_path = DBUS_WEBVIEW_EXPORT_PATH + + webview.get_page_id(); + let proxy = new this._ProxyConstructor(connection, + this._extension_name, webview_object_path); + if (cancellable) + proxy.TranslateRemote(cancellable, + this._translate_callback.bind(this, task)); + else + proxy.TranslateRemote(this._translate_callback.bind(this, + task)); + }, + null); // do nothing when name vanishes + return task; + }, + + _translate_callback: function (task, result, error) { + Gio.DBus.unwatch_name(task.watch_id); + if (error) + task.error = error; + if (task.callback) + task.callback(this, task); + }, + + /** + * Method: translate_html_finish + * Get the result of + * + * Parameters: + * res - result object passed to your callback + * + * Finishes an operation started by . + * If the operation ended in an error, this function will throw that error. + */ + translate_html_finish: function (res) { + if (res.error) + throw res.error; + }, + + /** + * Method: unregister + * Break the connection to WebKit + * + * Breaks the connection to all webviews and removes all DBus objects. + * You should call this in your application's *vfunc_dbus_unregister()* + * implementation. + * + * After this function has been called, no WebHelper functionality will + * work. + */ + unregister: function () { + this._dbus_impl.unexport_from_connection(this.connection); + }, +}); diff --git a/webhelper/webhelper_private/config.js.in b/webhelper/webhelper_private/config.js.in new file mode 100644 index 0000000..f9d87cb --- /dev/null +++ b/webhelper/webhelper_private/config.js.in @@ -0,0 +1 @@ +const LIBEXECDIR = '%libexecdir%'; -- cgit v1.2.3