diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile.am | 38 | ||||
-rw-r--r-- | configure.ac | 9 | ||||
-rw-r--r-- | jasmine.json | 4 | ||||
-rw-r--r-- | test/Makefile.am.inc | 2 | ||||
-rw-r--r-- | test/webhelper/testTranslate2Old.js | 213 | ||||
-rw-r--r-- | test/webhelper/testWebActions2Old.js | 120 | ||||
-rw-r--r-- | webhelper/webhelper2.js | 531 | ||||
-rw-r--r-- | webhelper/webhelper2_old.js | 11 | ||||
-rw-r--r-- | webhelper/webhelper_private/common.js | 530 |
10 files changed, 934 insertions, 526 deletions
@@ -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); + }, +}); |