extensions: Add Extension/Preferences base classes
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>
This commit is contained in:
parent
6a34b2636d
commit
3e4fd4b67a
@ -1,24 +1,21 @@
|
|||||||
import {getCurrentExtension, setExtensionManager} from './sharedInternals.js';
|
import {ExtensionBase, setExtensionManager} from './sharedInternals.js';
|
||||||
|
|
||||||
export {
|
export {gettext, ngettext, pgettext} from './sharedInternals.js';
|
||||||
getSettings,
|
|
||||||
initTranslations,
|
|
||||||
gettext,
|
|
||||||
ngettext,
|
|
||||||
pgettext
|
|
||||||
} from './sharedInternals.js';
|
|
||||||
|
|
||||||
const {extensionManager} = imports.ui.main;
|
const {extensionManager} = imports.ui.main;
|
||||||
setExtensionManager(extensionManager);
|
setExtensionManager(extensionManager);
|
||||||
|
|
||||||
/**
|
export class Extension extends ExtensionBase {
|
||||||
* Open the preference dialog of the current extension
|
enable() {
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the extension's preferences window
|
||||||
*/
|
*/
|
||||||
export function openPrefs() {
|
openPreferences() {
|
||||||
const extension = getCurrentExtension();
|
extensionManager.openExtensionPrefs(this.uuid, '', {});
|
||||||
|
}
|
||||||
if (!extension)
|
|
||||||
throw new Error('openPrefs() can only be called from extensions');
|
|
||||||
|
|
||||||
extensionManager.openExtensionPrefs(extension.uuid, '', {});
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import {setExtensionManager} from './sharedInternals.js';
|
import GObject from 'gi://GObject';
|
||||||
|
|
||||||
|
import {ExtensionBase, setExtensionManager} from './sharedInternals.js';
|
||||||
import {extensionManager} from '../extensionsService.js';
|
import {extensionManager} from '../extensionsService.js';
|
||||||
|
|
||||||
setExtensionManager(extensionManager);
|
setExtensionManager(extensionManager);
|
||||||
|
|
||||||
export {
|
export {gettext, ngettext, pgettext} from './sharedInternals.js';
|
||||||
getSettings,
|
|
||||||
initTranslations,
|
export class ExtensionPreferences extends ExtensionBase {
|
||||||
gettext,
|
/**
|
||||||
ngettext,
|
* Fill the preferences window with preferences.
|
||||||
pgettext
|
*
|
||||||
} from './sharedInternals.js';
|
* @param {Adw.PreferencesWindow} _window - the preferences window
|
||||||
|
*/
|
||||||
|
fillPreferencesWindow(_window) {
|
||||||
|
throw new GObject.NotImplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import GLib from 'gi://GLib';
|
import GLib from 'gi://GLib';
|
||||||
|
|
||||||
import * as Gettext from 'gettext';
|
import {bindtextdomain} from 'gettext';
|
||||||
|
|
||||||
const Config = imports.misc.config;
|
const Config = imports.misc.config;
|
||||||
|
|
||||||
@ -17,13 +17,154 @@ export function setExtensionManager(extensionManager) {
|
|||||||
_extensionManager = extensionManager;
|
_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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getCurrentExtension:
|
* @private
|
||||||
*
|
*
|
||||||
* @returns {?object} - The current extension, or null if not called from
|
* @returns {?object} - The current extension, or null if not called from
|
||||||
* an extension.
|
* an extension.
|
||||||
*/
|
*/
|
||||||
export function getCurrentExtension() {
|
function getCurrentExtension() {
|
||||||
const basePath = '/gnome-shell/extensions/';
|
const basePath = '/gnome-shell/extensions/';
|
||||||
|
|
||||||
// Search for an occurrence of an extension stack frame
|
// Search for an occurrence of an extension stack frame
|
||||||
@ -51,37 +192,12 @@ export function getCurrentExtension() {
|
|||||||
const dirName = GLib.path_get_basename(path);
|
const dirName = GLib.path_get_basename(path);
|
||||||
const extension = _extensionManager.lookup(dirName);
|
const extension = _extensionManager.lookup(dirName);
|
||||||
if (extension !== undefined)
|
if (extension !== undefined)
|
||||||
return extension;
|
return extension.stateObj;
|
||||||
} while (path !== '/');
|
} while (path !== '/');
|
||||||
|
|
||||||
return null;
|
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
|
|
||||||
*/
|
|
||||||
export 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
|
* Translate @str using the extension's gettext domain
|
||||||
*
|
*
|
||||||
@ -131,44 +247,5 @@ function callExtensionGettextFunc(func, ...args) {
|
|||||||
if (!extension)
|
if (!extension)
|
||||||
throw new Error(`${func}() can only be called from extensions`);
|
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);
|
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
|
|
||||||
*/
|
|
||||||
export 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});
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user