gnome-shell/js/misc/extensionUtils.js

295 lines
8.3 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported ExtensionState, ExtensionType, getCurrentExtension,
getSettings, initTranslations, gettext, ngettext, pgettext,
openPrefs, serializeExtension, deserializeExtension, setExtensionManager */
// Common utils for the extension system and the extension
// preferences tool
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gettext = imports.gettext;
const Config = imports.misc.config;
let _extensionManager = null;
var ExtensionType = {
SYSTEM: 1,
PER_USER: 2,
};
var ExtensionState = {
ENABLED: 1,
DISABLED: 2,
ERROR: 3,
OUT_OF_DATE: 4,
DOWNLOADING: 5,
INITIALIZED: 6,
DISABLING: 7,
ENABLING: 8,
// Used as an error state for operations on unknown extensions,
// should never be in a real extensionMeta object.
UNINSTALLED: 99,
};
const SERIALIZED_PROPERTIES = [
'type',
'state',
'path',
'error',
'hasPrefs',
'hasUpdate',
'canChange',
];
/**
* @param {object} extensionManager to use in utilities like `initTranslations()`
*/
function setExtensionManager(extensionManager) {
if (_extensionManager)
throw new Error('Trying to override existing extension manager');
_extensionManager = extensionManager;
}
/**
* getCurrentExtension:
*
* @returns {?object} - The current extension, or null if not called from
* an extension.
*/
function getCurrentExtension() {
const basePath = '/gnome-shell/extensions/';
// Search for an occurrence of an extension stack frame
// Start at 1 because 0 is the stack frame of this function
const [, ...stack] = new Error().stack.split('\n');
const extensionLine = stack.find(
line => line.includes(basePath));
if (!extensionLine)
return null;
// local import, as the module is used from outside the gnome-shell process
// as well
if (!_extensionManager)
setExtensionManager(imports.ui.main.extensionManager);
// The exact stack line differs depending on where the function
// was called (function or module scope), and whether it's called
// from a module or legacy import (file:// URI vs. plain path).
//
// We don't have to care about the exact composition, all we need
// is a string that can be traversed as path and contains the UUID
let path = extensionLine.slice(extensionLine.indexOf(basePath));
2014-11-29 18:05:11 +00:00
// Walk up the directory tree, looking for an extension with
// the same UUID as a directory name.
do {
path = GLib.path_get_dirname(path);
const dirName = GLib.path_get_basename(path);
const extension = _extensionManager.lookup(dirName);
if (extension !== undefined)
return extension;
} while (path !== '/');
return null;
}
/**
* Initialize Gettext to load translations from extensionsdir/locale.
* If @domain is not provided, it will be taken from metadata['gettext-domain']
*
* @param {string=} domain - the gettext domain to use
*/
function initTranslations(domain) {
let extension = getCurrentExtension();
if (!extension)
throw new Error('initTranslations() can only be called from extensions');
domain ||= extension.metadata['gettext-domain'];
// Expect USER extensions to have a locale/ subfolder, otherwise assume a
// SYSTEM extension that has been installed in the same prefix as the shell
let localeDir = extension.dir.get_child('locale');
if (localeDir.query_exists(null))
Gettext.bindtextdomain(domain, localeDir.get_path());
else
Gettext.bindtextdomain(domain, Config.LOCALEDIR);
Object.assign(extension, Gettext.domain(domain));
}
/**
* Translate @str using the extension's gettext domain
*
* @param {string} str - the string to translate
*
* @returns {string} - the translated string
*/
function gettext(str) {
return callExtensionGettextFunc('gettext', str);
}
/**
* Translate @str and choose plural form using the extension's
* gettext domain
*
* @param {string} str - the string to translate
* @param {string} strPlural - the plural form of the string
* @param {number} n - the quantity for which translation is needed
*
* @returns {string} - the translated string
*/
function ngettext(str, strPlural, n) {
return callExtensionGettextFunc('ngettext', str, strPlural, n);
}
/**
* Translate @str in the context of @context using the extension's
* gettext domain
*
* @param {string} context - context to disambiguate @str
* @param {string} str - the string to translate
*
* @returns {string} - the translated string
*/
function pgettext(context, str) {
return callExtensionGettextFunc('pgettext', context, str);
}
/**
* @private
* @param {string} func - function name
* @param {*[]} args - function arguments
*/
function callExtensionGettextFunc(func, ...args) {
const extension = getCurrentExtension();
if (!extension)
throw new Error(`${func}() can only be called from extensions`);
if (!extension[func])
throw new Error(`${func}() is used without calling initTranslations() first`);
return extension[func](...args);
}
/**
* Builds and returns a GSettings schema for @schema, using schema files
* in extensionsdir/schemas. If @schema is omitted, it is taken from
* metadata['settings-schema'].
*
* @param {string=} schema - the GSettings schema id
* @returns {Gio.Settings} - a new settings object for @schema
*/
function getSettings(schema) {
let extension = getCurrentExtension();
if (!extension)
throw new Error('getSettings() can only be called from extensions');
schema ||= extension.metadata['settings-schema'];
const GioSSS = Gio.SettingsSchemaSource;
// Expect USER extensions to have a schemas/ subfolder, otherwise assume a
// SYSTEM extension that has been installed in the same prefix as the shell
let schemaDir = extension.dir.get_child('schemas');
let schemaSource;
if (schemaDir.query_exists(null)) {
schemaSource = GioSSS.new_from_directory(
schemaDir.get_path(), GioSSS.get_default(), false);
} else {
schemaSource = GioSSS.get_default();
}
let schemaObj = schemaSource.lookup(schema, true);
if (!schemaObj)
throw new Error(`Schema ${schema} could not be found for extension ${extension.metadata.uuid}. Please check your installation`);
return new Gio.Settings({settings_schema: schemaObj});
}
/**
* Open the preference dialog of the current extension
*/
function openPrefs() {
const extension = getCurrentExtension();
if (!extension)
throw new Error('openPrefs() can only be called from extensions');
try {
const extensionManager = imports.ui.main.extensionManager;
extensionManager.openExtensionPrefs(extension.uuid, '', {});
} catch (e) {
if (e.name === 'ImportError')
throw new Error('openPrefs() cannot be called from preferences');
logError(e, 'Failed to open extension preferences');
}
}
/**
* Serialize extension into an object that can be used
* in a vardict {GLib.Variant}
*
* @param {object} extension - an extension object
* @returns {object}
*/
function serializeExtension(extension) {
let obj = {...extension.metadata};
SERIALIZED_PROPERTIES.forEach(prop => {
obj[prop] = extension[prop];
});
let res = {};
for (let key in obj) {
let val = obj[key];
let type;
switch (typeof val) {
case 'string':
type = 's';
break;
case 'number':
type = 'd';
break;
case 'boolean':
type = 'b';
break;
default:
continue;
}
res[key] = GLib.Variant.new(type, val);
}
return res;
}
/**
* Deserialize an unpacked variant into an extension object
*
* @param {object} variant - an unpacked {GLib.Variant}
* @returns {object}
*/
function deserializeExtension(variant) {
let res = {metadata: {}};
for (let prop in variant) {
let val = variant[prop].unpack();
if (SERIALIZED_PROPERTIES.includes(prop))
res[prop] = val;
else
res.metadata[prop] = val;
}
// add the 2 additional properties to create a valid extension object, as createExtensionObject()
res.uuid = res.metadata.uuid;
res.dir = Gio.File.new_for_path(res.path);
return res;
}