
Extensions now must export a class that conforms to a particular interface both for the actual extension as well as for prefs: enable()/disable() methods for the former, fillPreferencesWindow() for the latter. This is quite similar to the previous method-based entry points, but it also gives us a more structured way of providing convenience API in form of base classes. Do that in form of Extension and ExtensionPreferences classes on top of a common ExtensionBase base class. getSettings(), initTranslations() and the gettext wrappers are now methods of the common base, while openPreferences() moves to the Extension class. Based on an original suggestion from Evan Welsh. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2838>
252 lines
7.3 KiB
JavaScript
252 lines
7.3 KiB
JavaScript
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
|
|
import {bindtextdomain} from 'gettext';
|
|
|
|
const Config = imports.misc.config;
|
|
|
|
let _extensionManager = null;
|
|
|
|
/**
|
|
* @param {object} extensionManager to use in utilities like `initTranslations()`
|
|
*/
|
|
export function setExtensionManager(extensionManager) {
|
|
if (_extensionManager)
|
|
throw new Error('Trying to override existing extension manager');
|
|
|
|
_extensionManager = extensionManager;
|
|
}
|
|
|
|
export class ExtensionBase {
|
|
#gettextDomain;
|
|
|
|
/**
|
|
* @param {object} metadata - metadata passed in when loading the extension
|
|
*/
|
|
constructor(metadata) {
|
|
if (this.constructor === ExtensionBase)
|
|
throw new Error('ExtensionBase cannot be used directly.');
|
|
|
|
if (!metadata)
|
|
throw new Error(`${this.constructor.name} did not pass metadata to parent`);
|
|
|
|
this.metadata = metadata;
|
|
}
|
|
|
|
/**
|
|
* @type {string}
|
|
*/
|
|
get uuid() {
|
|
return this.metadata['uuid'];
|
|
}
|
|
|
|
/**
|
|
* @type {Gio.File}
|
|
*/
|
|
get dir() {
|
|
return this.metadata['dir'];
|
|
}
|
|
|
|
/**
|
|
* @type {string}
|
|
*/
|
|
get path() {
|
|
return this.metadata['path'];
|
|
}
|
|
|
|
/**
|
|
* Get a GSettings object 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}
|
|
*/
|
|
getSettings(schema) {
|
|
schema ||= this.metadata['settings-schema'];
|
|
|
|
// Expect USER extensions to have a schemas/ subfolder, otherwise assume a
|
|
// SYSTEM extension that has been installed in the same prefix as the shell
|
|
const schemaDir = this.dir.get_child('schemas');
|
|
const defaultSource = Gio.SettingsSchemaSource.get_default();
|
|
let schemaSource;
|
|
if (schemaDir.query_exists(null)) {
|
|
schemaSource = Gio.SettingsSchemaSource.new_from_directory(
|
|
schemaDir.get_path(), defaultSource, false);
|
|
} else {
|
|
schemaSource = defaultSource;
|
|
}
|
|
|
|
const schemaObj = schemaSource.lookup(schema, true);
|
|
if (!schemaObj)
|
|
throw new Error(`Schema ${schema} could not be found for extension ${this.uuid}. Please check your installation`);
|
|
|
|
return new Gio.Settings({settings_schema: schemaObj});
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
initTranslations(domain) {
|
|
domain ||= this.metadata['gettext-domain'];
|
|
|
|
if (!domain)
|
|
throw new Error('initTranslations() was called without providing a valid 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
|
|
const localeDir = this.dir.get_child('locale');
|
|
if (localeDir.query_exists(null))
|
|
bindtextdomain(domain, localeDir.get_path());
|
|
else
|
|
bindtextdomain(domain, Config.LOCALEDIR);
|
|
|
|
this.#gettextDomain = domain;
|
|
}
|
|
|
|
/**
|
|
* Translate `str` using the extension's gettext domain
|
|
*
|
|
* @param {string} str - the string to translate
|
|
*
|
|
* @returns {string} the translated string
|
|
*/
|
|
gettext(str) {
|
|
this.#checkGettextDomain('gettext');
|
|
return GLib.dgettext(this.#gettextDomain, 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
|
|
*/
|
|
ngettext(str, strPlural, n) {
|
|
this.#checkGettextDomain('ngettext');
|
|
return GLib.dngettext(this.#gettextDomain, 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
|
|
*/
|
|
pgettext(context, str) {
|
|
this.#checkGettextDomain('pgettext');
|
|
return GLib.dpgettext2(this.#gettextDomain, context, str);
|
|
}
|
|
|
|
/**
|
|
* @param {string} func
|
|
*/
|
|
#checkGettextDomain(func) {
|
|
if (!this.#gettextDomain)
|
|
throw new Error(`${func}() is used without calling initTranslations() first`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*
|
|
* @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;
|
|
|
|
// 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.stateObj;
|
|
} while (path !== '/');
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Translate @str using the extension's gettext domain
|
|
*
|
|
* @param {string} str - the string to translate
|
|
*
|
|
* @returns {string} - the translated string
|
|
*/
|
|
export 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
|
|
*/
|
|
export 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
|
|
*/
|
|
export 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`);
|
|
|
|
return extension[func](...args);
|
|
}
|