extensionUtils: Split out extension convenience functions
ExtensionUtils was originally used for shared functions between the extension system and the (old) prefs-tool, but then gained useful API meant for extensions themselves. It's a bit weird to mix the two, so split out the extension convenience API into a separate module. We will soon split up the module further, and add specific "flavors" for extensions and preferences, with the current code providing a shared base for both. That should explain both the new location and the odd module name. :-) Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2837>
This commit is contained in:
parent
23f525785c
commit
d3f662fbf2
@ -7,7 +7,7 @@ import {ExtensionPrefsDialog} from './extensionPrefsDialog.js';
|
|||||||
import {ServiceImplementation} from './dbusService.js';
|
import {ServiceImplementation} from './dbusService.js';
|
||||||
|
|
||||||
const {deserializeExtension} = imports.misc.extensionUtils;
|
const {deserializeExtension} = imports.misc.extensionUtils;
|
||||||
const {setExtensionManager} = imports.misc.extensionUtils;
|
const {setExtensionManager} = imports.extensions.sharedInternals;
|
||||||
const {loadInterfaceXML} = imports.misc.dbusUtils;
|
const {loadInterfaceXML} = imports.misc.dbusUtils;
|
||||||
|
|
||||||
const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
<file>extensionPrefsDialog.js</file>
|
<file>extensionPrefsDialog.js</file>
|
||||||
<file>dbusService.js</file>
|
<file>dbusService.js</file>
|
||||||
|
|
||||||
|
<file>extensions/sharedInternals.js</file>
|
||||||
|
|
||||||
<file>misc/config.js</file>
|
<file>misc/config.js</file>
|
||||||
<file>misc/extensionUtils.js</file>
|
<file>misc/extensionUtils.js</file>
|
||||||
<file>misc/dbusUtils.js</file>
|
<file>misc/dbusUtils.js</file>
|
||||||
|
201
js/extensions/sharedInternals.js
Normal file
201
js/extensions/sharedInternals.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
/* exported getCurrentExtension, setExtensionManager, getSettings,
|
||||||
|
initTranslations, gettext, ngettext, pgettext, openPrefs */
|
||||||
|
|
||||||
|
const Gio = imports.gi.Gio;
|
||||||
|
const GLib = imports.gi.GLib;
|
||||||
|
|
||||||
|
const Gettext = imports.gettext;
|
||||||
|
|
||||||
|
const Config = imports.misc.config;
|
||||||
|
|
||||||
|
let _extensionManager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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));
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@
|
|||||||
<file>gdm/realmd.js</file>
|
<file>gdm/realmd.js</file>
|
||||||
<file>gdm/util.js</file>
|
<file>gdm/util.js</file>
|
||||||
|
|
||||||
|
<file>extensions/sharedInternals.js</file>
|
||||||
|
|
||||||
<file>misc/animationUtils.js</file>
|
<file>misc/animationUtils.js</file>
|
||||||
<file>misc/config.js</file>
|
<file>misc/config.js</file>
|
||||||
<file>misc/extensionUtils.js</file>
|
<file>misc/extensionUtils.js</file>
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
/* exported ExtensionState, ExtensionType,
|
||||||
/* exported ExtensionState, ExtensionType, getCurrentExtension,
|
* serializeExtension, deserializeExtension */
|
||||||
getSettings, initTranslations, gettext, ngettext, pgettext,
|
|
||||||
openPrefs, serializeExtension, deserializeExtension, setExtensionManager */
|
|
||||||
|
|
||||||
// Common utils for the extension system and the extension
|
// Common utils for the extension system, the extensions D-Bus service
|
||||||
// preferences tool
|
// and the Extensions app
|
||||||
|
|
||||||
const Gio = imports.gi.Gio;
|
const Gio = imports.gi.Gio;
|
||||||
const GLib = imports.gi.GLib;
|
const GLib = imports.gi.GLib;
|
||||||
|
|
||||||
const Gettext = imports.gettext;
|
|
||||||
|
|
||||||
const Config = imports.misc.config;
|
|
||||||
|
|
||||||
let _extensionManager = null;
|
|
||||||
|
|
||||||
var ExtensionType = {
|
var ExtensionType = {
|
||||||
SYSTEM: 1,
|
SYSTEM: 1,
|
||||||
PER_USER: 2,
|
PER_USER: 2,
|
||||||
@ -45,196 +37,6 @@ const SERIALIZED_PROPERTIES = [
|
|||||||
'canChange',
|
'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));
|
|
||||||
|
|
||||||
// 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
|
* Serialize extension into an object that can be used
|
||||||
* in a vardict {GLib.Variant}
|
* in a vardict {GLib.Variant}
|
||||||
|
@ -21,7 +21,7 @@ overrides:
|
|||||||
- tests/shell/**
|
- tests/shell/**
|
||||||
excludedFiles:
|
excludedFiles:
|
||||||
- js/portalHelper/*
|
- js/portalHelper/*
|
||||||
- js/misc/extensionUtils.js
|
- js/extensions/*
|
||||||
globals:
|
globals:
|
||||||
global: readonly
|
global: readonly
|
||||||
_: readonly
|
_: readonly
|
||||||
|
Loading…
Reference in New Issue
Block a user