summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile.am38
-rw-r--r--configure.ac9
-rw-r--r--jasmine.json4
-rw-r--r--test/Makefile.am.inc2
-rw-r--r--test/webhelper/testTranslate2Old.js213
-rw-r--r--test/webhelper/testWebActions2Old.js120
-rw-r--r--webhelper/webhelper2.js531
-rw-r--r--webhelper/webhelper2_old.js11
-rw-r--r--webhelper/webhelper_private/common.js530
10 files changed, 934 insertions, 526 deletions
diff --git a/.gitignore b/.gitignore
index cba4d8a..11733fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@ Endless-0.gir
Endless-0.typelib
WebHelper2Private-1.0.gir
WebHelper2Private-1.0.typelib
+WebHelper2OldPrivate-1.0.gir
+WebHelper2OldPrivate-1.0.typelib
endless/eosresource.c
endless/eosresource-private.h
tools/eos-application-manifest/eos-application-manifest
diff --git a/Makefile.am b/Makefile.am
index 1444725..34f506f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -88,6 +88,7 @@ DISTCLEANFILES += @EOS_SDK_API_NAME@.pc
webhelper_sources = \
webhelper/webhelper.js \
webhelper/webhelper2.js \
+ webhelper/webhelper2_old.js \
$(NULL)
gjsmodulesdir = $(datadir)/gjs-1.0
@@ -96,7 +97,10 @@ webhelper_privatedir = $(webhelperdir)/webhelper_private
dist_webhelper_DATA = \
$(webhelper_sources) \
$(NULL)
-dist_webhelper_private_DATA = webhelper/webhelper_private/config.js
+dist_webhelper_private_DATA = \
+ webhelper/webhelper_private/common.js \
+ webhelper/webhelper_private/config.js \
+ $(NULL)
EOS_JS_COVERAGE_FILES = $(webhelper_sources)
@@ -123,6 +127,28 @@ wh2extension_la_CPPFLAGS = @WEBHELPER2_EXTENSION_CFLAGS@
wh2extension_la_LIBADD = @WEBHELPER2_EXTENSION_LIBS@
wh2extension_la_LDFLAGS = -module -avoid-version -no-undefined
+# WebKit2-3.0 version
+
+lib_LTLIBRARIES += libwebhelper2oldprivate.la
+libwebhelper2oldprivate_la_SOURCES = \
+ webhelper/lib/wh2private.c \
+ webhelper/lib/wh2private.h \
+ $(NULL)
+libwebhelper2oldprivate_la_CPPFLAGS = @WEBHELPER2_OLD_PRIVATE_CFLAGS@
+libwebhelper2oldprivate_la_LIBADD = @WEBHELPER2_OLD_PRIVATE_LIBS@
+libwebhelper2oldprivate_la_LDFLAGS = -avoid-version
+
+webhelper2oldextensionsdir = $(libexecdir)/webhelper2old
+webhelper2oldextensions_LTLIBRARIES = wh2oldextension.la
+wh2oldextension_la_SOURCES = \
+ webhelper/webextensions/wh2extension.c \
+ webhelper/webextensions/wh2jscutil.c \
+ webhelper/webextensions/wh2jscutil.h \
+ $(NULL)
+wh2oldextension_la_CPPFLAGS = @WEBHELPER2_OLD_EXTENSION_CFLAGS@
+wh2oldextension_la_LIBADD = @WEBHELPER2_OLD_EXTENSION_LIBS@
+wh2oldextension_la_LDFLAGS = -module -avoid-version -no-undefined
+
# # # INTROSPECTION FILES # # #
-include $(INTROSPECTION_MAKEFILE)
@@ -158,6 +184,16 @@ WebHelper2Private_1_0_gir_LIBS = libwebhelper2private.la
WebHelper2Private_1_0_gir_FILES = $(libwebhelper2private_la_SOURCES)
INTROSPECTION_GIRS += WebHelper2Private-1.0.gir
+WebHelper2OldPrivate-1.0.gir: libwebhelper2oldprivate.la
+WebHelper2OldPrivate_1_0_gir_INCLUDES = GObject-2.0 GLib-2.0 WebKit2-3.0
+WebHelper2OldPrivate_1_0_gir_SCANNERFLAGS = \
+ --identifier-prefix=Wh2 \
+ --symbol-prefix=wh2 \
+ $(NULL)
+WebHelper2OldPrivate_1_0_gir_LIBS = libwebhelper2oldprivate.la
+WebHelper2OldPrivate_1_0_gir_FILES = $(libwebhelper2oldprivate_la_SOURCES)
+INTROSPECTION_GIRS += WebHelper2OldPrivate-1.0.gir
+
girdir = $(datadir)/gir-1.0
gir_DATA = $(INTROSPECTION_GIRS)
diff --git a/configure.ac b/configure.ac
index 875ce4c..322c3d3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -84,6 +84,7 @@ GIO_REQUIREMENT="gio-2.0"
GTK_REQUIREMENT="gtk+-3.0 >= 3.16"
JSON_GLIB_REQUIREMENT="json-glib-1.0 >= 0.12"
WEBKIT2_REQUIREMENT="webkit2gtk-4.0"
+WEBKIT2_OLD_REQUIREMENT="webkit2gtk-3.0"
# These go into the pkg-config file as Requires: and Requires.private:
# (Generally, use Requires.private: instead of Requires:)
EOS_REQUIRED_MODULES=
@@ -224,10 +225,18 @@ PKG_CHECK_MODULES([WEBHELPER2_EXTENSION], [
PKG_CHECK_MODULES([WEBHELPER2_PRIVATE], [
$GLIB_REQUIREMENT
$WEBKIT2_REQUIREMENT])
+PKG_CHECK_MODULES([WEBHELPER2_OLD_EXTENSION], [
+ $GLIB_REQUIREMENT
+ $GOBJECT_REQUIREMENT
+ $WEBKIT2_OLD_REQUIREMENT])
+PKG_CHECK_MODULES([WEBHELPER2_OLD_PRIVATE], [
+ $GLIB_REQUIREMENT
+ $WEBKIT2_OLD_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], [3.0])
EOS_CHECK_GJS_GIR([WebKit2], [4.0])
# Code coverage reports support
diff --git a/jasmine.json b/jasmine.json
index 440dca1..7e2e39f 100644
--- a/jasmine.json
+++ b/jasmine.json
@@ -10,8 +10,10 @@
],
"exclude": [
"test/webhelper/testTranslate.js",
+ "test/webhelper/testTranslate2Old.js",
"test/webhelper/testUpdateFontSize.js",
- "test/webhelper/testWebActions.js"
+ "test/webhelper/testWebActions.js",
+ "test/webhelper/testWebActions2Old.js"
],
"environment": {
"GI_TYPELIB_PATH": ".",
diff --git a/test/Makefile.am.inc b/test/Makefile.am.inc
index 211bd8f..73a2172 100644
--- a/test/Makefile.am.inc
+++ b/test/Makefile.am.inc
@@ -47,8 +47,10 @@ javascript_tests = \
test/webhelper/testLocal.js \
test/webhelper/testTranslate.js \
test/webhelper/testTranslate2.js \
+ test/webhelper/testTranslate2Old.js \
test/webhelper/testWebActions.js \
test/webhelper/testWebActions2.js \
+ test/webhelper/testWebActions2Old.js \
test/webhelper/testUpdateFontSize.js \
test/endless/testCustomContainer.js \
test/endless/testTopbarNavButton.js \
diff --git a/test/webhelper/testTranslate2Old.js b/test/webhelper/testTranslate2Old.js
new file mode 100644
index 0000000..66d54ce
--- /dev/null
+++ b/test/webhelper/testTranslate2Old.js
@@ -0,0 +1,213 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Mainloop = imports.mainloop;
+const WebHelper2 = imports.webhelper2_old;
+const WebKit2 = imports.gi.WebKit2;
+
+const WELL_KNOWN_NAME = 'com.endlessm.WebHelper.testTranslate2';
+
+/* CAUTION:
+ * All tests trying to use the translation functionality of WebHelper2 must be
+ * run in this file, and this file must be run before any other WebHelper2
+ * tests in the same process.
+ * That is because we can only tell the default web context to load web
+ * extensions with user data once per process. WebHelper doesn't support web
+ * contexts other than the default one.
+ */
+
+Gtk.init(null);
+
+describe('WebHelper2 WebKit2-3.0 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 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();
+ });
+
+ it('complains about a bad ngettext function', function () {
+ expect(function () {
+ webhelper.set_ngettext('I am not a function');
+ }).toThrow();
+ });
+
+ it('gets and sets the ngettext function', function () {
+ let translation_function = (s, p, n) => n == 1 ? s : p;
+ webhelper.set_ngettext(translation_function);
+ expect(webhelper.get_ngettext()).toBe(translation_function);
+ });
+
+ it('has a null ngettext function by default', function () {
+ expect(webhelper.get_ngettext()).toBeNull();
+ });
+
+ it('can remove the ngettext function by setting null', function () {
+ webhelper.set_ngettext((s, p, n) => n == 1 ? s : p);
+ expect(webhelper.get_ngettext()).not.toBeNull();
+ webhelper.set_ngettext(null);
+ expect(webhelper.get_ngettext()).toBeNull();
+ });
+
+ describe('translating a page', function () {
+ let webview, gettext_spy;
+ const MINIMAL_HTML = '<p name="translatable">Translate Me</p>';
+
+ function run_loop(html=MINIMAL_HTML) {
+ let error_spy = jasmine.createSpy('error_spy');
+ webview.connect('load-failed', error_spy);
+ let id = webview.connect('load-changed', (webview, event) => {
+ if (event === WebKit2.LoadEvent.FINISHED) {
+ webhelper.translate_html(webview, null, (obj, res) => {
+ expect(function () {
+ webhelper.translate_html_finish(res);
+ }).not.toThrow();
+ webview.disconnect(id);
+ expect(error_spy).not.toHaveBeenCalled();
+ Mainloop.quit('webhelper2');
+ });
+ }
+ });
+ webview.load_html('<html><body>' + html + '</body></html>', null);
+ Mainloop.run('webhelper2');
+ }
+
+ beforeEach(function () {
+ webview = new WebKit2.WebView();
+ gettext_spy = jasmine.createSpy('gettext_spy').and.callFake((s) => s);
+ webhelper.set_gettext(gettext_spy);
+ });
+
+ it('translates a string', function () {
+ 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);
+ let error_spy = jasmine.createSpy('error_spy');
+ webview.connect('load-failed', error_spy);
+ let id = 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();
+ webview.disconnect(id);
+ expect(error_spy).not.toHaveBeenCalled();
+ done();
+ });
+ }
+ });
+ webview.load_html('<html><body></body></html>', null);
+ });
+
+ it('normalizes a string before translating it', function () {
+ run_loop('<p name="translatable">\n\
+ Translate Me\n\
+ </p>');
+ expect(gettext_spy).toHaveBeenCalledWith('Translate Me');
+ });
+
+ it('handles quotes correctly', function () {
+ run_loop('<p name="translatable">String with "quotes"</p>');
+ expect(gettext_spy).toHaveBeenCalledWith('String with "quotes"');
+ });
+
+ it('handles embedded tags correctly', function () {
+ run_loop('<p name="translatable">Embedded<br><b>tags</b></p>');
+ expect(gettext_spy).toHaveBeenCalledWith('Embedded<br><b>tags</b>');
+ });
+ });
+
+ describe('used from client-side Javascript', function () {
+ let webview;
+
+ beforeEach(function () {
+ webview = new WebKit2.WebView();
+ });
+
+ function load_script(view, script) {
+ view.load_html('<html><body><script type="text/javascript">' +
+ script + '</script></body></html>', null);
+ Mainloop.run('webhelper2');
+ }
+
+ it('translates a string with gettext()', function (done) {
+ let gettext_spy = jasmine.createSpy('gettext_spy').and.callFake((s) => {
+ Mainloop.quit('webhelper2');
+ return s;
+ });
+ webhelper.set_gettext(gettext_spy);
+ load_script(webview, 'gettext("Translate Me");');
+ expect(gettext_spy).toHaveBeenCalledWith('Translate Me');
+ done();
+ });
+
+ it('translates a string with ngettext()', function (done) {
+ let ngettext_spy = jasmine.createSpy('ngettext_spy').and.callFake((s, p, n) => {
+ Mainloop.quit('webhelper2');
+ return n == 1 ? s : p;
+ });
+ webhelper.set_ngettext(ngettext_spy);
+ load_script(webview, 'ngettext("File", "Files", 3);');
+ expect(ngettext_spy).toHaveBeenCalledWith('File', 'Files', 3);
+ done();
+ });
+ });
+});
diff --git a/test/webhelper/testWebActions2Old.js b/test/webhelper/testWebActions2Old.js
new file mode 100644
index 0000000..a602dc1
--- /dev/null
+++ b/test/webhelper/testWebActions2Old.js
@@ -0,0 +1,120 @@
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Mainloop = imports.mainloop;
+const WebHelper2 = imports.webhelper2_old;
+const WebKit2 = imports.gi.WebKit2;
+
+const WELL_KNOWN_NAME = 'com.endlessm.WebHelper.testWebActions2';
+
+Gtk.init(null);
+
+describe('WebKit2-3.0 actions bindings', function () {
+ let owner_id, connection, webview, webhelper, web_action_spy;
+
+ beforeAll(function (done) {
+ owner_id = Gio.DBus.own_name(Gio.BusType.SESSION, WELL_KNOWN_NAME,
+ Gio.BusNameOwnerFlags.NONE,
+ null, // bus acquired
+ (con) => { // name acquired
+ connection = con;
+ done();
+ },
+ null); // name lost
+ });
+
+ afterAll(function () {
+ Gio.DBus.unown_name(owner_id);
+ });
+
+ function run_loop(action_to_test) {
+ let string = '<html><head><meta http-equiv="refresh" content="0;url=' +
+ action_to_test + '"></head><body></body></html>';
+ webview.load_html(string, null);
+ Mainloop.run('webhelper2');
+ }
+
+ beforeEach(function () {
+ webhelper = new WebHelper2.WebHelper({
+ well_known_name: WELL_KNOWN_NAME,
+ connection: connection,
+ });
+ webview = new WebKit2.WebView();
+ web_action_spy = jasmine.createSpy('web_action_spy').and.callFake(() =>
+ Mainloop.quit('webhelper2'));
+ webhelper.define_web_action('action', web_action_spy);
+ });
+
+ afterEach(function () {
+ webhelper.unregister();
+ });
+
+ it('calls a web action', function () {
+ run_loop('webhelper://action');
+ expect(web_action_spy).toHaveBeenCalled();
+ });
+
+ it('calls a web action with a parameter', function () {
+ run_loop('webhelper://action?param=value');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ param: 'value',
+ }));
+ });
+
+ it('calls a web action with many parameters', function () {
+ run_loop('webhelper://action?first=thefirst&second=thesecond&third=thethird');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ first: 'thefirst',
+ second: 'thesecond',
+ third: 'thethird',
+ }));
+ });
+
+ it('uri-decodes parameter names', function () {
+ run_loop('webhelper://action?p%C3%A4r%C3%A4m%F0%9F%92%A9=value');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ 'päräm💩': 'value',
+ }));
+ });
+
+ it('uri-decodes parameter values', function () {
+ run_loop('webhelper://action?param=v%C3%A1lu%C3%A9%F0%9F%92%A9');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ param: 'válué💩',
+ }));
+ });
+
+ // This is commented out because GJS cannot catch exceptions across FFI
+ // interfaces (e.g. in GObject callbacks.)
+ xit('raises an exception on a nonexistent action instead of calling it', function () {
+ expect(function () {
+ run_loop('webhelper://nonexistentAction?param=value');
+ }).toThrow();
+ });
+
+ it('calls a web action with a blank parameter', function () {
+ run_loop('webhelper://action?param=');
+ expect(web_action_spy).toHaveBeenCalledWith(jasmine.objectContaining({
+ param: '',
+ }));
+ });
+
+ it('uri-decodes web action names', function () {
+ webhelper.define_web_action('äction💩Quit', web_action_spy);
+ run_loop('webhelper://%C3%A4ction%F0%9F%92%A9Quit');
+ expect(web_action_spy).toHaveBeenCalled();
+ });
+
+ it('can define more than one action with define_web_actions()', function () {
+ webhelper.define_web_actions({
+ action2: web_action_spy,
+ });
+ run_loop('webhelper://action2');
+ expect(web_action_spy).toBeTruthy();
+ });
+
+ it('complains when defining an action that is not a function', function () {
+ expect(function () {
+ webhelper.define_web_action('badAction', 'not a function');
+ }).toThrow();
+ });
+});
diff --git a/webhelper/webhelper2.js b/webhelper/webhelper2.js
index e781330..6b28b10 100644
--- a/webhelper/webhelper2.js
+++ b/webhelper/webhelper2.js
@@ -1,528 +1,11 @@
// Copyright 2015 Endless Mobile, Inc.
-imports.gi.versions.WebKit2 = '4.0';
-
-const Format = imports.format;
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-const GObject = imports.gi.GObject;
-const Lang = imports.lang;
-const WebHelper2Private = imports.gi.WebHelper2Private;
-const WebKit2 = imports.gi.WebKit2;
-
-const Config = imports.webhelper_private.config;
-
-String.prototype.format = Format.format;
-
-const WH2_URI_SCHEME = 'webhelper';
-const WH2_LOCAL_FILE_SCHEME = 'local';
-const DBUS_WEBVIEW_EXPORT_PATH = '/com/endlessm/webview/';
-const WH2_DBUS_EXTENSION_INTERFACE = '\
- <node> \
- <interface name="com.endlessm.WebHelper2.Translation"> \
- <method name="Translate"/> \
- </interface> \
- </node>';
-const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
- <node> \
- <interface name="com.endlessm.WebHelper2.Gettext"> \
- <method name="Gettext"> \
- <arg name="message" type="s" direction="in"/> \
- <arg name="translation" type="s" direction="out"/> \
- </method> \
- <method name="NGettext"> \
- <arg name="message_singular" type="s" direction="in"/> \
- <arg name="message_plural" type="s" direction="in"/> \
- <arg name="number" type="t" direction="in"/> \
- <arg name="translation" type="s" direction="out"/> \
- </method> \
- </interface> \
- </node>';
-
-/**
- * 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.
- *
- * Although WebKit provides an easy way of communicating from GTK code to
- * the in-browser Javascript, through the execute_script() method, it is not so
- * easy to communicate the other way around.
- *
- * WebHelper solves that problem by detecting when the web page navigates to a
- * custom action URI.
- * The custom URI corresponds to a function that you define using
- * <WebHelper.define_web_action()>, and you can pass parameters to the
- * function.
- *
- * Another 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
- * <WebHelper.translate_html()>.
- * It also exposes a *gettext()* function in the client-side Javascript.
- *
- * For cases where you need to load local files for your web applications,
- * WebHelper also provides the local:// URI scheme.
- * For this to work, you must also load your main page via the local:// URI
- * scheme.
- */
-
-/**
- * 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 <WebHelper> in
- * its *vfunc_dbus_register()* implementation, with appropriate
- * <well-known-name> and <connection> 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 <Application.web_actions_handler()>.
- *
- * 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 <the rules for well-known bus names at
- * http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names>.
- *
- * 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._web_actions = {};
- this._gettext = null;
- this._ngettext = 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');
-
- // Set up handling for custom URIs
- WebHelper2Private.register_uri_scheme(WH2_URI_SCHEME,
- this._on_endless_uri_request.bind(this));
- WebHelper2Private.register_uri_scheme(WH2_LOCAL_FILE_SCHEME,
- this._on_local_file_request.bind(this));
- },
-
- _on_endless_uri_request: function (request) {
- let uri = request.get_uri();
-
- // get the name and parameters for the desired function
- let f_call = uri.substr((WH2_URI_SCHEME + '://').length).split('?');
- let function_name = decodeURI(f_call[0]);
-
- if (!this._web_actions.hasOwnProperty(function_name))
- throw new Error(('Undefined WebHelper action "%s". Did you define it with ' +
- 'WebHelper.Application.define_web_action()?').format(function_name));
-
- let parameters = {};
- if (f_call[1]) {
- // there are parameters
- let params = f_call[1].split('&');
- params.forEach(function (entry) {
- let param = entry.split('=');
+/* exported WebHelper */
- if (param.length == 2) {
- param[0] = decodeURIComponent(param[0]);
- param[1] = decodeURIComponent(param[1]);
- // and now we add it...
- parameters[param[0]] = param[1];
- }
- });
- }
-
- (this._web_actions[function_name].bind(this))(parameters);
-
- // Don't call request.finish(), because we don't want to finish the
- // action, which would involve loading a new page. The request dies
- // if we return from this function without calling ref() or finish()
- // on it.
- },
-
- _on_local_file_request: function (request) {
- let path = request.get_path();
- let file = Gio.File.new_for_path(path);
- let [content_type, certain] = Gio.content_type_guess(path, null);
- try {
- let stream = file.read(null);
- request.finish(stream, -1, content_type);
- } catch (error) {
- request.finish_error(error);
- }
- },
-
- // DBus implementations
-
- Gettext: function (string) {
- return this._gettext(string);
- },
-
- NGettext: function (singular, plural, number) {
- return this._ngettext(singular, plural, number);
- },
-
- // Public API
-
- /**
- * Method: set_gettext
- * Define function which translates text
- *
- * Parameters:
- * gettext_func - a function, or null
- *
- * When you plan 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().
- *
- * This function will be called with each string to translate when you call
- * <translate_html()>.
- * The function is also made available directly to the browser-side
- * Javascript as *gettext()*, a property of the global object.
- *
- * 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 <translate_html()>.
- *
- * 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 <set_gettext()>, or null
- * if none is currently set.
- */
- get_gettext: function () {
- return this._gettext;
- },
-
- /**
- * Method: set_ngettext
- * Define function which translates singular/plural text
- *
- * Parameters:
- * ngettext_func - a function, or null
- *
- * When you plan to translate text in your web application, set this
- * property to the translation function.
- * The function must take three parameters: a string singular message, a
- * string plural message, and a number for which to generate the message.
- * The function must return a string.
- * The canonical example is ngettext().
- *
- * This function is made available directly to the browser-side Javascript
- * as *ngettext()*, a property of the global object.
- *
- * Pass null for _ngettext_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 *ngettext()* in the client-side
- * Javascript.
- *
- * Example:
- * > const WebHelper2 = imports.webhelper2;
- * > const Gettext = imports.gettext;
- * > const GETTEXT_DOMAIN = 'smoke-grinder';
- * >
- * > let webhelper = new WebHelper2.WebHelper('com.example.SmokeGrinder');
- * > webhelper.set_gettext(Gettext.dngettext.bind(null, GETTEXT_DOMAIN));
- */
- set_ngettext: function (ngettext_func) {
- if (ngettext_func !== null && typeof ngettext_func !== 'function')
- throw new Error('The translation function must be a function, or ' +
- 'null.');
- this._ngettext = ngettext_func;
- },
-
- /**
- * Method: get_ngettext
- * Retrieve the currently set singular/plural translation function
- *
- * Returns:
- * the translation function previously set with <set_ngettext()>, or null
- * if none is currently set.
- */
- get_ngettext: function () {
- return this._ngettext;
- },
-
- /**
- * 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 <WebHelper>
- * 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
- * <set_gettext()>.
- *
- * When the translation is finished, _callback_ will be called.
- * You can get the result of the operation by calling
- * <translate_html_finish()> 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();
- // Warning: this._ProxyConstructor will do a synchronous
- // operation unless you pass in a callback
- new this._ProxyConstructor(connection, this._extension_name,
- webview_object_path, (proxy, error) =>
- {
- if (error) {
- this._translate_callback(task, null, error);
- return;
- }
- if (cancellable)
- proxy.TranslateRemote(cancellable,
- this._translate_callback.bind(this, task));
- else
- proxy.TranslateRemote(this._translate_callback.bind(this,
- task));
- }, cancellable);
- },
- 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 <translate_html()>
- *
- * Parameters:
- * res - result object passed to your callback
- *
- * Finishes an operation started by <translate_html()>.
- * 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: define_web_action
- * Define an action that may be invoked from a WebView
- *
- * Parameters:
- * name - a string, which must be a valid URI location.
- * implementation - a function (see Callback Parameters below.)
- *
- * Callback Parameters:
- * dict - object containing properties corresponding to the query
- * parameters that the web action was called with
- *
- * Sets up an action that may be invoked from an HTML document inside a
- * WebView, or from the in-browser Javascript environment inside a WebView.
- * If you set up an action "setVolume" as follows:
- * > app.define_web_action('setVolume', function(dict) { ... });
- * Then you can invoke the action inside the HTML document, e.g. as the
- * target of a link, as follows:
- * > <a href="endless://setVolume?volume=11">This one goes to 11</a>
- * Or from the in-browser Javascript, by navigating to the action URI, as
- * follows:
- * > window.location.href = 'endless://setVolume?volume=11';
- *
- * In both cases, the function would then be called with the _dict_
- * parameter equal to
- * > { "volume": "11" }
- *
- * If an action called _name_ is already defined, the new _implementation_
- * replaces the old one.
- */
- define_web_action: function (name, implementation) {
- if (typeof implementation !== 'function') {
- throw new Error('The implementation of a web action must be a ' +
- 'function.');
- }
- this._web_actions[name] = implementation;
- },
-
- /**
- * Method: define_web_actions
- * Define several web actions at once
- *
- * Parameters:
- * dict - an object, with web action names as property names, and their
- * implementations as values
- *
- * Convenience method to define more than one web action at once.
- * Calls <define_web_action()> on each property of _dict_.
- *
- * *Note* This API is Javascript-only. It will not be implemented in C.
- */
- define_web_actions: function (dict) {
- Object.keys(dict).forEach((key) =>
- this.define_web_action(key, dict[key]));
- },
+imports.gi.versions.WebKit2 = '4.0';
+window.WebHelper2Private = imports.gi.WebHelper2Private;
+window.WebKit2 = imports.gi.WebKit2;
+window.LIBEXEC_SUBDIR = 'webhelper2';
- /**
- * 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);
- },
-});
+const Common = imports.webhelper_private.common;
+const WebHelper = Common.WebHelper;
diff --git a/webhelper/webhelper2_old.js b/webhelper/webhelper2_old.js
new file mode 100644
index 0000000..809504e
--- /dev/null
+++ b/webhelper/webhelper2_old.js
@@ -0,0 +1,11 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+/* exported WebHelper */
+
+imports.gi.versions.WebKit2 = '3.0';
+window.WebHelper2Private = imports.gi.WebHelper2OldPrivate;
+window.WebKit2 = imports.gi.WebKit2;
+window.LIBEXEC_SUBDIR = 'webhelper2old';
+
+const Common = imports.webhelper_private.common;
+const WebHelper = Common.WebHelper;
diff --git a/webhelper/webhelper_private/common.js b/webhelper/webhelper_private/common.js
new file mode 100644
index 0000000..4a93d53
--- /dev/null
+++ b/webhelper/webhelper_private/common.js
@@ -0,0 +1,530 @@
+// Copyright 2015 Endless Mobile, Inc.
+
+// The following constant and two modules must be in the global namespace before
+// importing this file: LIBEXEC_SUBDIR, WebKit2, WebHelper2Private
+
+/* exported WebHelper */
+
+const Format = imports.format;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+
+String.prototype.format = Format.format;
+
+const Config = imports.webhelper_private.config;
+
+const WH2_URI_SCHEME = 'webhelper';
+const WH2_LOCAL_FILE_SCHEME = 'local';
+const DBUS_WEBVIEW_EXPORT_PATH = '/com/endlessm/webview/';
+const WH2_DBUS_EXTENSION_INTERFACE = '\
+ <node> \
+ <interface name="com.endlessm.WebHelper2.Translation"> \
+ <method name="Translate"/> \
+ </interface> \
+ </node>';
+const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
+ <node> \
+ <interface name="com.endlessm.WebHelper2.Gettext"> \
+ <method name="Gettext"> \
+ <arg name="message" type="s" direction="in"/> \
+ <arg name="translation" type="s" direction="out"/> \
+ </method> \
+ <method name="NGettext"> \
+ <arg name="message_singular" type="s" direction="in"/> \
+ <arg name="message_plural" type="s" direction="in"/> \
+ <arg name="number" type="t" direction="in"/> \
+ <arg name="translation" type="s" direction="out"/> \
+ </method> \
+ </interface> \
+ </node>';
+
+
+/**
+ * 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.
+ *
+ * Although WebKit provides an easy way of communicating from GTK code to
+ * the in-browser Javascript, through the execute_script() method, it is not so
+ * easy to communicate the other way around.
+ *
+ * WebHelper solves that problem by detecting when the web page navigates to a
+ * custom action URI.
+ * The custom URI corresponds to a function that you define using
+ * <WebHelper.define_web_action()>, and you can pass parameters to the
+ * function.
+ *
+ * Another 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
+ * <WebHelper.translate_html()>.
+ * It also exposes a *gettext()* function in the client-side Javascript.
+ *
+ * For cases where you need to load local files for your web applications,
+ * WebHelper also provides the local:// URI scheme.
+ * For this to work, you must also load your main page via the local:// URI
+ * scheme.
+ */
+
+/**
+ * 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 <WebHelper> in
+ * its *vfunc_dbus_register()* implementation, with appropriate
+ * <well-known-name> and <connection> 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 <Application.web_actions_handler()>.
+ *
+ * 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 <the rules for well-known bus names at
+ * http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names>.
+ *
+ * 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._web_actions = {};
+ this._gettext = null;
+ this._ngettext = 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 = window.WebKit2.WebContext.get_default();
+ context.connect('initialize-web-extensions', () => {
+ let libexec = Gio.File.new_for_path(Config.LIBEXECDIR);
+ let path = libexec.get_child(window.LIBEXEC_SUBDIR).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');
+
+ // Set up handling for custom URIs
+ window.WebHelper2Private.register_uri_scheme(WH2_URI_SCHEME,
+ this._on_endless_uri_request.bind(this));
+ window.WebHelper2Private.register_uri_scheme(WH2_LOCAL_FILE_SCHEME,
+ this._on_local_file_request.bind(this));
+ },
+
+ _on_endless_uri_request: function (request) {
+ let uri = request.get_uri();
+
+ // get the name and parameters for the desired function
+ let f_call = uri.substr((WH2_URI_SCHEME + '://').length).split('?');
+ let function_name = decodeURI(f_call[0]);
+
+ if (!this._web_actions.hasOwnProperty(function_name))
+ throw new Error(('Undefined WebHelper action "%s". Did you define it with ' +
+ 'WebHelper.Application.define_web_action()?').format(function_name));
+
+ let parameters = {};
+ if (f_call[1]) {
+ // there are parameters
+ let params = f_call[1].split('&');
+ params.forEach(function (entry) {
+ let param = entry.split('=');
+
+ if (param.length == 2) {
+ param[0] = decodeURIComponent(param[0]);
+ param[1] = decodeURIComponent(param[1]);
+ // and now we add it...
+ parameters[param[0]] = param[1];
+ }
+ });
+ }
+
+ (this._web_actions[function_name].bind(this))(parameters);
+
+ // Don't call request.finish(), because we don't want to finish the
+ // action, which would involve loading a new page. The request dies
+ // if we return from this function without calling ref() or finish()
+ // on it.
+ },
+
+ _on_local_file_request: function (request) {
+ let path = request.get_path();
+ let file = Gio.File.new_for_path(path);
+ let [content_type, certain] = Gio.content_type_guess(path, null);
+ try {
+ let stream = file.read(null);
+ request.finish(stream, -1, content_type);
+ } catch (error) {
+ request.finish_error(error);
+ }
+ },
+
+ // DBus implementations
+
+ Gettext: function (string) {
+ return this._gettext(string);
+ },
+
+ NGettext: function (singular, plural, number) {
+ return this._ngettext(singular, plural, number);
+ },
+
+ // Public API
+
+ /**
+ * Method: set_gettext
+ * Define function which translates text
+ *
+ * Parameters:
+ * gettext_func - a function, or null
+ *
+ * When you plan 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().
+ *
+ * This function will be called with each string to translate when you call
+ * <translate_html()>.
+ * The function is also made available directly to the browser-side
+ * Javascript as *gettext()*, a property of the global object.
+ *
+ * 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 <translate_html()>.
+ *
+ * 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 <set_gettext()>, or null
+ * if none is currently set.
+ */
+ get_gettext: function () {
+ return this._gettext;
+ },
+
+ /**
+ * Method: set_ngettext
+ * Define function which translates singular/plural text
+ *
+ * Parameters:
+ * ngettext_func - a function, or null
+ *
+ * When you plan to translate text in your web application, set this
+ * property to the translation function.
+ * The function must take three parameters: a string singular message, a
+ * string plural message, and a number for which to generate the message.
+ * The function must return a string.
+ * The canonical example is ngettext().
+ *
+ * This function is made available directly to the browser-side Javascript
+ * as *ngettext()*, a property of the global object.
+ *
+ * Pass null for _ngettext_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 *ngettext()* in the client-side
+ * Javascript.
+ *
+ * Example:
+ * > const WebHelper2 = imports.webhelper2;
+ * > const Gettext = imports.gettext;
+ * > const GETTEXT_DOMAIN = 'smoke-grinder';
+ * >
+ * > let webhelper = new WebHelper2.WebHelper('com.example.SmokeGrinder');
+ * > webhelper.set_gettext(Gettext.dngettext.bind(null, GETTEXT_DOMAIN));
+ */
+ set_ngettext: function (ngettext_func) {
+ if (ngettext_func !== null && typeof ngettext_func !== 'function')
+ throw new Error('The translation function must be a function, or ' +
+ 'null.');
+ this._ngettext = ngettext_func;
+ },
+
+ /**
+ * Method: get_ngettext
+ * Retrieve the currently set singular/plural translation function
+ *
+ * Returns:
+ * the translation function previously set with <set_ngettext()>, or null
+ * if none is currently set.
+ */
+ get_ngettext: function () {
+ return this._ngettext;
+ },
+
+ /**
+ * 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 <WebHelper>
+ * 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
+ * <set_gettext()>.
+ *
+ * When the translation is finished, _callback_ will be called.
+ * You can get the result of the operation by calling
+ * <translate_html_finish()> 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 appeared
+ let webview_object_path = DBUS_WEBVIEW_EXPORT_PATH +
+ webview.get_page_id();
+ // Warning: this._ProxyConstructor will do a synchronous
+ // operation unless you pass in a callback
+ new this._ProxyConstructor(connection, this._extension_name,
+ webview_object_path, (proxy, error) =>
+ {
+ if (error) {
+ this._translate_callback(task, null, error);
+ return;
+ }
+ if (cancellable)
+ proxy.TranslateRemote(cancellable,
+ this._translate_callback.bind(this, task));
+ else
+ proxy.TranslateRemote(this._translate_callback.bind(this,
+ task));
+ }, cancellable);
+ },
+ 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 <translate_html()>
+ *
+ * Parameters:
+ * res - result object passed to your callback
+ *
+ * Finishes an operation started by <translate_html()>.
+ * 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: define_web_action
+ * Define an action that may be invoked from a WebView
+ *
+ * Parameters:
+ * name - a string, which must be a valid URI location.
+ * implementation - a function (see Callback Parameters below.)
+ *
+ * Callback Parameters:
+ * dict - object containing properties corresponding to the query
+ * parameters that the web action was called with
+ *
+ * Sets up an action that may be invoked from an HTML document inside a
+ * WebView, or from the in-browser Javascript environment inside a WebView.
+ * If you set up an action "setVolume" as follows:
+ * > app.define_web_action('setVolume', function(dict) { ... });
+ * Then you can invoke the action inside the HTML document, e.g. as the
+ * target of a link, as follows:
+ * > <a href="endless://setVolume?volume=11">This one goes to 11</a>
+ * Or from the in-browser Javascript, by navigating to the action URI, as
+ * follows:
+ * > window.location.href = 'endless://setVolume?volume=11';
+ *
+ * In both cases, the function would then be called with the _dict_
+ * parameter equal to
+ * > { "volume": "11" }
+ *
+ * If an action called _name_ is already defined, the new _implementation_
+ * replaces the old one.
+ */
+ define_web_action: function (name, implementation) {
+ if (typeof implementation !== 'function') {
+ throw new Error('The implementation of a web action must be a ' +
+ 'function.');
+ }
+ this._web_actions[name] = implementation;
+ },
+
+ /**
+ * Method: define_web_actions
+ * Define several web actions at once
+ *
+ * Parameters:
+ * dict - an object, with web action names as property names, and their
+ * implementations as values
+ *
+ * Convenience method to define more than one web action at once.
+ * Calls <define_web_action()> on each property of _dict_.
+ *
+ * *Note* This API is Javascript-only. It will not be implemented in C.
+ */
+ define_web_actions: function (dict) {
+ Object.keys(dict).forEach((key) =>
+ this.define_web_action(key, dict[key]));
+ },
+
+ /**
+ * 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);
+ },
+});