2012-01-19 00:55:20 +00:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2019-01-31 14:07:06 +00:00
|
|
|
/* exported ExtensionState, ExtensionType, getCurrentExtension,
|
2020-04-12 00:45:55 +00:00
|
|
|
getSettings, initTranslations, gettext, ngettext, pgettext,
|
|
|
|
openPrefs, isOutOfDate, installImporter, serializeExtension,
|
|
|
|
deserializeExtension */
|
2012-01-19 00:55:20 +00:00
|
|
|
|
|
|
|
// Common utils for the extension system and the extension
|
|
|
|
// preferences tool
|
|
|
|
|
2018-11-01 12:55:17 +00:00
|
|
|
const { Gio, GLib } = imports.gi;
|
|
|
|
|
2018-07-14 18:16:13 +00:00
|
|
|
const Gettext = imports.gettext;
|
2012-06-04 21:14:18 +00:00
|
|
|
|
2012-01-19 00:55:20 +00:00
|
|
|
const Config = imports.misc.config;
|
|
|
|
|
2017-07-18 17:47:27 +00:00
|
|
|
var ExtensionType = {
|
2012-01-19 00:55:20 +00:00
|
|
|
SYSTEM: 1,
|
2019-08-20 21:43:54 +00:00
|
|
|
PER_USER: 2,
|
2012-01-19 00:55:20 +00:00
|
|
|
};
|
|
|
|
|
2019-07-06 13:31:57 +00:00
|
|
|
var ExtensionState = {
|
|
|
|
ENABLED: 1,
|
|
|
|
DISABLED: 2,
|
|
|
|
ERROR: 3,
|
|
|
|
OUT_OF_DATE: 4,
|
|
|
|
DOWNLOADING: 5,
|
|
|
|
INITIALIZED: 6,
|
|
|
|
|
|
|
|
// Used as an error state for operations on unknown extensions,
|
|
|
|
// should never be in a real extensionMeta object.
|
2019-08-20 21:43:54 +00:00
|
|
|
UNINSTALLED: 99,
|
2019-07-06 13:31:57 +00:00
|
|
|
};
|
|
|
|
|
2020-01-22 13:45:15 +00:00
|
|
|
const SERIALIZED_PROPERTIES = [
|
|
|
|
'type',
|
|
|
|
'state',
|
|
|
|
'path',
|
|
|
|
'error',
|
|
|
|
'hasPrefs',
|
|
|
|
'hasUpdate',
|
|
|
|
'canChange',
|
|
|
|
];
|
2018-11-01 12:55:17 +00:00
|
|
|
|
2016-10-03 23:08:18 +00:00
|
|
|
/**
|
|
|
|
* getCurrentExtension:
|
|
|
|
*
|
2019-10-17 16:41:52 +00:00
|
|
|
* @returns {?object} - The current extension, or null if not called from
|
|
|
|
* an extension.
|
2016-10-03 23:08:18 +00:00
|
|
|
*/
|
2012-01-31 01:58:29 +00:00
|
|
|
function getCurrentExtension() {
|
2019-11-16 16:14:38 +00:00
|
|
|
let stack = new Error().stack.split('\n');
|
2016-10-03 23:08:18 +00:00
|
|
|
let extensionStackLine;
|
2012-01-31 01:58:29 +00:00
|
|
|
|
2016-10-03 23:08:18 +00:00
|
|
|
// Search for an occurrence of an extension stack frame
|
|
|
|
// Start at 1 because 0 is the stack frame of this function
|
|
|
|
for (let i = 1; i < stack.length; i++) {
|
2018-07-14 20:56:22 +00:00
|
|
|
if (stack[i].includes('/gnome-shell/extensions/')) {
|
2016-10-03 23:08:18 +00:00
|
|
|
extensionStackLine = stack[i];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2012-01-31 01:58:29 +00:00
|
|
|
if (!extensionStackLine)
|
2016-10-03 23:08:18 +00:00
|
|
|
return null;
|
2012-01-31 01:58:29 +00:00
|
|
|
|
|
|
|
// The stack line is like:
|
|
|
|
// init([object Object])@/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
|
|
|
|
//
|
|
|
|
// In the case that we're importing from
|
|
|
|
// module scope, the first field is blank:
|
|
|
|
// @/home/user/data/gnome-shell/extensions/u@u.id/prefs.js:8
|
|
|
|
let match = new RegExp('@(.+):\\d+').exec(extensionStackLine);
|
|
|
|
if (!match)
|
2016-10-03 23:08:18 +00:00
|
|
|
return null;
|
2012-01-31 01:58:29 +00:00
|
|
|
|
2019-07-07 21:38:27 +00:00
|
|
|
// local import, as the module is used from outside the gnome-shell process
|
|
|
|
// as well (not this function though)
|
|
|
|
let extensionManager = imports.ui.main.extensionManager;
|
|
|
|
|
2012-01-31 01:58:29 +00:00
|
|
|
let path = match[1];
|
2012-05-28 21:21:02 +00:00
|
|
|
let file = Gio.File.new_for_path(path);
|
2012-01-31 01:58:29 +00:00
|
|
|
|
2014-11-29 18:05:11 +00:00
|
|
|
// Walk up the directory tree, looking for an extension with
|
2012-05-28 21:21:02 +00:00
|
|
|
// the same UUID as a directory name.
|
|
|
|
while (file != null) {
|
2019-07-07 22:01:11 +00:00
|
|
|
let extension = extensionManager.lookup(file.get_basename());
|
2012-05-28 21:21:02 +00:00
|
|
|
if (extension !== undefined)
|
|
|
|
return extension;
|
|
|
|
file = file.get_parent();
|
|
|
|
}
|
2012-01-31 01:58:29 +00:00
|
|
|
|
2016-10-03 23:08:18 +00:00
|
|
|
return null;
|
2012-01-31 01:58:29 +00:00
|
|
|
}
|
|
|
|
|
2018-07-14 18:16:13 +00:00
|
|
|
/**
|
|
|
|
* initTranslations:
|
2019-10-17 16:41:52 +00:00
|
|
|
* @param {string=} domain - the gettext domain to use
|
2018-07-14 18:16:13 +00:00
|
|
|
*
|
|
|
|
* Initialize Gettext to load translations from extensionsdir/locale.
|
|
|
|
* If @domain is not provided, it will be taken from metadata['gettext-domain']
|
|
|
|
*/
|
|
|
|
function initTranslations(domain) {
|
|
|
|
let extension = getCurrentExtension();
|
|
|
|
|
|
|
|
if (!extension)
|
|
|
|
throw new Error('initTranslations() can only be called from extensions');
|
|
|
|
|
|
|
|
domain = 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);
|
2020-04-12 00:45:55 +00:00
|
|
|
|
|
|
|
Object.assign(extension, Gettext.domain(domain));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gettext:
|
|
|
|
* @param {string} str - the string to translate
|
|
|
|
*
|
|
|
|
* Translate @str using the extension's gettext domain
|
|
|
|
*
|
|
|
|
* @returns {string} - the translated string
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
function gettext(str) {
|
|
|
|
return callExtensionGettextFunc('gettext', str);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ngettext:
|
|
|
|
* @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
|
|
|
|
*
|
|
|
|
* Translate @str and choose plural form using the extension's
|
|
|
|
* gettext domain
|
|
|
|
*
|
|
|
|
* @returns {string} - the translated string
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
function ngettext(str, strPlural, n) {
|
|
|
|
return callExtensionGettextFunc('ngettext', str, strPlural, n);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* pgettext:
|
|
|
|
* @param {string} context - context to disambiguate @str
|
|
|
|
* @param {string} str - the string to translate
|
|
|
|
*
|
|
|
|
* Translate @str in the context of @context using the extension's
|
|
|
|
* gettext domain
|
|
|
|
*
|
|
|
|
* @returns {string} - the translated string
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
function pgettext(context, str) {
|
|
|
|
return callExtensionGettextFunc('pgettext', context, str);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2018-07-14 18:16:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* getSettings:
|
2019-10-17 16:41:52 +00:00
|
|
|
* @param {string=} schema - the GSettings schema id
|
|
|
|
* @returns {Gio.Settings} - a new settings object for @schema
|
2018-07-14 18:16:13 +00:00
|
|
|
*
|
|
|
|
* 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'].
|
|
|
|
*/
|
|
|
|
function getSettings(schema) {
|
|
|
|
let extension = getCurrentExtension();
|
|
|
|
|
|
|
|
if (!extension)
|
|
|
|
throw new Error('getSettings() can only be called from extensions');
|
|
|
|
|
|
|
|
schema = 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;
|
2019-08-20 00:51:42 +00:00
|
|
|
if (schemaDir.query_exists(null)) {
|
2018-07-14 18:16:13 +00:00
|
|
|
schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
|
|
|
|
GioSSS.get_default(),
|
|
|
|
false);
|
2019-08-20 00:51:42 +00:00
|
|
|
} else {
|
2018-07-14 18:16:13 +00:00
|
|
|
schemaSource = GioSSS.get_default();
|
2019-08-20 00:51:42 +00:00
|
|
|
}
|
2018-07-14 18:16:13 +00:00
|
|
|
|
|
|
|
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 });
|
|
|
|
}
|
|
|
|
|
2020-04-01 19:08:38 +00:00
|
|
|
/**
|
|
|
|
* openPrefs:
|
|
|
|
*
|
|
|
|
* 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');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-31 01:58:29 +00:00
|
|
|
function isOutOfDate(extension) {
|
2021-02-25 23:47:48 +00:00
|
|
|
const [major] = Config.PACKAGE_VERSION.split('.');
|
|
|
|
return !extension.metadata['shell-version'].some(v => v.startsWith(major));
|
2012-01-19 00:55:20 +00:00
|
|
|
}
|
|
|
|
|
2018-11-01 12:55:17 +00:00
|
|
|
function serializeExtension(extension) {
|
2020-11-13 22:54:56 +00:00
|
|
|
let obj = { ...extension.metadata };
|
2018-11-01 12:55:17 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2012-01-31 01:58:29 +00:00
|
|
|
function installImporter(extension) {
|
2016-10-05 05:27:18 +00:00
|
|
|
let oldSearchPath = imports.searchPath.slice(); // make a copy
|
2016-10-17 21:05:05 +00:00
|
|
|
imports.searchPath = [extension.dir.get_parent().get_path()];
|
2016-10-05 05:27:18 +00:00
|
|
|
// importing a "subdir" creates a new importer object that doesn't affect
|
|
|
|
// the global one
|
2016-10-17 21:05:05 +00:00
|
|
|
extension.imports = imports[extension.uuid];
|
2016-10-05 05:27:18 +00:00
|
|
|
imports.searchPath = oldSearchPath;
|
2012-01-19 00:55:20 +00:00
|
|
|
}
|