// 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 DBUS_WEBVIEW_EXPORT_PATH = '/com/endlessm/webview/';
const WH2_DBUS_EXTENSION_INTERFACE = '\
\
\
\
\
';
const WH2_DBUS_MAIN_PROGRAM_INTERFACE = '\
\
\
\
\
\
\
\
\
\
\
\
\
\
';
/**
* Namespace: WebHelper2
* Convenience library for running web applications
*
* WebHelper is a convenience library for displaying web applications inside a
* GTK container running WebKitGTK.
* WebHelper2 is the WebKit2 version.
*
* 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
* , 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
* .
* It also exposes a *gettext()* function in the client-side Javascript.
*/
/**
* Class: WebHelper
* Helper object for a WebKit2 web application
*
* Constructor parameters:
* props - a dictionary of construction properties and values (default {})
*
* The application class for your web application should create in
* its *vfunc_dbus_register()* implementation, with appropriate
* and parameters.
* After that, you can do further setup on it, such as defining web actions, in
* your *vfunc_startup()* implementation.
*
* There is no need to set up specially any web views that you create, unlike
* WebKit1 where you must set up .
*
* Example:
* > const TestApplication = new Lang.Class({
* > Name: 'TestApplication',
* > Extends: Gtk.Application,
* >
* > vfunc_dbus_register: function (connection, object_path) {
* > this._webhelper = new WebHelper2.WebHelper({
* > well_known_name: this.application_id,
* > connection: connection,
* > });
* > return this.parent(connection, object_path);
* > },
* >
* > vfunc_startup: function () {
* > this.parent();
* >
* > this._webhelper.set_gettext(Gettext.dgettext.bind(null,
* > GETTEXT_DOMAIN));
* >
* > let window = new Gtk.Window();
* > let webview = new WebKit2.WebView();
* > webview.connect('load-changed', (webview, event) => {
* > if (event === WebKit2.LoadEvent.FINISHED)
* > this._webhelper.translate_html(webview, null, (obj, res) => {
* > this._webhelper.translate_html_finish(res);
* > window.show_all();
* > });
* > });
* > window.add(webview);
* > webview.load_uri('file:///path/to/my/page.html');
* > },
* >
* > vfunc_dbus_unregister: function (connection, object_path) {
* > this.parent(connection, object_path);
* > this._webhelper.unregister();
* > },
* >});
* >
* >let app = new TestApplication({
* > application_id: 'com.example.SmokeGrinder',
* >});
* >app.run(ARGV);
*/
const WebHelper = new Lang.Class({
Name: 'WebHelper',
GTypeName: 'Wh2WebHelper',
Extends: GObject.Object,
Properties: {
/**
* Property: well-known-name
* Well-known bus name owned by the calling program
*
* Type:
* string
*
* This property is required at construction time.
* It must conform to .
*
* This must be a well-known bus name that your program owns.
* The easiest way to ensure that is to use your application's ID, since
* every application class registers its ID as a bus name.
*/
'well-known-name': GObject.ParamSpec.string('well-known-name',
'Well-known name',
'Well-known bus name owned by the calling program',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
''),
/**
* Property: connection
* DBus connection owned by the calling program
*
* Type:
* *Gio.DBusConnection*
*
* This property is required at construction time.
*
* This must be a DBus connection object that your program owns.
* The easiest way to ensure that is to use the connection object passed
* in to your application's *vfunc_dbus_register()* function.
*/
'connection': GObject.ParamSpec.object('connection', 'Connection',
'DBus connection owned by the calling program',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
Gio.DBusConnection.$gtype),
},
_init: function (props={}) {
this._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 webhelper:// URIs
WebHelper2Private.register_uri_scheme(WH2_URI_SCHEME,
this._on_endless_uri_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.
},
// 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
* .
* 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 .
*
* Example:
* > const Gettext = imports.gettext;
* > const GETTEXT_DOMAIN = 'smoke-grinder';
* >
* > webhelper.set_gettext(Gettext.dgettext.bind(null, GETTEXT_DOMAIN));
*/
set_gettext: function (gettext_func) {
if (gettext_func !== null && typeof gettext_func !== 'function')
throw new Error('The translation function must be a function, or ' +
'null.');
this._gettext = gettext_func;
},
/**
* Method: get_gettext
* Retrieve the currently set translation function
*
* Returns:
* the translation function previously set with , or null
* if none is currently set.
*/
get_gettext: function () {
return this._gettext;
},
/**
* Method: 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 , 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
* object, and a result object; or null if you don't want a callback
*
* Perform translation on all HTML elements marked with name="translatable"
* in the HTML document displaying in _webview_.
* The translation will be performed using the function you have set using
* .
*
* When the translation is finished, _callback_ will be called.
* You can get the result of the operation by calling
* with the _result_ object passed to _callback_.
*
* You can optionally pass _cancellable_ if you want to be able to cancel
* the operation.
*
* Example:
* > webview.connect('load-changed', (webview, event) => {
* > if (event === WebKit2.LoadEvent.FINISHED)
* > webhelper.translate_html(webview, null, (obj, res) => {
* > webhelper.translate_html_finish(res);
* > // Translation done, show the page
* > webview.show_all();
* > });
* > });
*/
translate_html: function (webview, cancellable, callback) {
let task = { callback: callback };
// Wait for the DBus interface to appear on the bus
task.watch_id = Gio.DBus.watch_name(Gio.BusType.SESSION,
this._extension_name, Gio.BusNameWatcherFlags.NONE,
(connection, name, owner) => {
// name appeared
let webview_object_path = DBUS_WEBVIEW_EXPORT_PATH +
webview.get_page_id();
let proxy = new this._ProxyConstructor(connection,
this._extension_name, webview_object_path);
if (cancellable)
proxy.TranslateRemote(cancellable,
this._translate_callback.bind(this, task));
else
proxy.TranslateRemote(this._translate_callback.bind(this,
task));
},
null); // do nothing when name vanishes
return task;
},
_translate_callback: function (task, result, error) {
Gio.DBus.unwatch_name(task.watch_id);
if (error)
task.error = error;
if (task.callback)
task.callback(this, task);
},
/**
* Method: translate_html_finish
* Get the result of
*
* Parameters:
* res - result object passed to your callback
*
* Finishes an operation started by .
* If the operation ended in an error, this function will throw that error.
*/
translate_html_finish: function (res) {
if (res.error)
throw res.error;
},
/**
* Method: 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:
* > This one goes to 11
* 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 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);
},
});