extensions-app: Split out ExtensionManager

The extension handling is currently intertwined with the UI.

Splitting it out provides a clearer separation, and will allow
us to switch to a model-based UI later.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3067>
This commit is contained in:
Florian Müllner
2023-12-18 20:20:26 +01:00
committed by Marge Bot
parent 3d070fdc6b
commit f7ded3e509
6 changed files with 384 additions and 206 deletions

View File

@ -10,7 +10,6 @@ import * as Gettext from 'gettext';
import * as Config from './misc/config.js';
import {ExtensionRow} from './extensionRow.js';
import {ExtensionState, ExtensionType, deserializeExtension} from './misc/extensionUtils.js';
Gio._promisify(Gio.DBusConnection.prototype, 'call');
Gio._promisify(Shew.WindowExporter.prototype, 'export');
@ -36,8 +35,6 @@ export const ExtensionsWindow = GObject.registerClass({
if (Config.PROFILE === 'development')
this.add_css_class('devel');
this._updatesCheckId = 0;
this._exporter = new Shew.WindowExporter({window: this});
this._exportedHandle = '';
@ -52,7 +49,8 @@ export const ExtensionsWindow = GObject.registerClass({
name: 'user-extensions-enabled',
state: 'false',
change_state: (a, state) => {
this._shellProxy.UserExtensionsEnabled = state.get_boolean();
const {extensionManager} = this.application;
extensionManager.userExtensionsEnabled = state.get_boolean();
},
}]);
@ -90,27 +88,38 @@ export const ExtensionsWindow = GObject.registerClass({
}));
this._systemList.connect('row-activated', (_list, row) => row.activate());
this._shellProxy.connectSignal('ExtensionStateChanged',
this._onExtensionStateChanged.bind(this));
this._shellProxy.connect('g-properties-changed',
const {extensionManager} = this.application;
extensionManager.connect('notify::failed',
() => this._syncVisiblePage());
extensionManager.connect('notify::n-updates',
() => this._checkUpdates());
extensionManager.connect('notify::user-extensions-enabled',
this._onUserExtensionsEnabledChanged.bind(this));
this._onUserExtensionsEnabledChanged();
this._scanExtensions();
extensionManager.connect('extension-added',
(mgr, extension) => this._addExtensionRow(extension));
extensionManager.connect('extension-removed',
(mgr, extension) => this._removeExtensionRow(extension));
extensionManager.connect('extension-changed',
(mgr, extension) => {
const row = this._findExtensionRow(extension);
const isUser = row?.get_parent() === this._userList;
if (extension.isUser !== isUser) {
this._removeExtensionRow(extension);
this._addExtensionRow(extension);
}
});
extensionManager.connect('extensions-loaded',
() => this._extensionsLoaded());
}
get _shellProxy() {
return this.application.shellProxy;
}
uninstall(uuid) {
const row = this._findExtensionRow(uuid);
uninstall(extension) {
const dialog = new Gtk.MessageDialog({
transient_for: this,
modal: true,
text: _('Remove “%s”?').format(row.name),
text: _('Remove “%s”?').format(extension.name),
secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'),
});
@ -119,14 +128,16 @@ export const ExtensionsWindow = GObject.registerClass({
.get_style_context().add_class('destructive-action');
dialog.connect('response', (dlg, response) => {
const {extensionManager} = this.application;
if (response === Gtk.ResponseType.ACCEPT)
this._shellProxy.UninstallExtensionAsync(uuid).catch(console.error);
extensionManager.uninstallExtension(extension.uuid);
dialog.destroy();
});
dialog.present();
}
async openPrefs(uuid) {
async openPrefs(extension) {
if (!this._exportedHandle) {
try {
this._exportedHandle = await this._exporter.export();
@ -135,9 +146,8 @@ export const ExtensionsWindow = GObject.registerClass({
}
}
this._shellProxy.OpenExtensionPrefsAsync(uuid,
this._exportedHandle,
{modal: new GLib.Variant('b', true)}).catch(console.error);
const {extensionManager} = this.application;
extensionManager.openExtensionPrefs(extension.uuid, this._exportedHandle);
}
_showAbout() {
@ -180,104 +190,68 @@ export const ExtensionsWindow = GObject.registerClass({
}
_sortList(row1, row2) {
return row1.name.localeCompare(row2.name);
const {name: name1} = row1.extension;
const {name: name2} = row2.extension;
return name1.localeCompare(name2);
}
_filterList(row) {
const {keywords} = row.extension;
return this._searchTerms.every(
t => row.keywords.some(k => k.startsWith(t)));
t => keywords.some(k => k.startsWith(t)));
}
_findExtensionRow(uuid) {
_findExtensionRow(extension) {
return [
...this._userList,
...this._systemList,
].find(c => c.uuid === uuid);
].find(c => c.extension === extension);
}
_onUserExtensionsEnabledChanged() {
const {userExtensionsEnabled} = this.application.extensionManager;
const action = this.lookup_action('user-extensions-enabled');
action.set_state(
new GLib.Variant('b', this._shellProxy.UserExtensionsEnabled));
}
_onExtensionStateChanged(proxy, senderName, [uuid, newState]) {
const extension = deserializeExtension(newState);
let row = this._findExtensionRow(uuid);
this._queueUpdatesCheck();
// the extension's type changed; remove the corresponding row
// and reset the variable to null so that we create a new row
// below and add it to the appropriate list
if (row && row.type !== extension.type) {
row.get_parent().remove(row);
row = null;
}
if (row) {
if (extension.state === ExtensionState.UNINSTALLED)
row.get_parent().remove(row);
} else {
this._addExtensionRow(extension);
}
this._syncListVisibility();
}
async _scanExtensions() {
try {
const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
for (let uuid in extensionsMap) {
const extension = deserializeExtension(extensionsMap[uuid]);
this._addExtensionRow(extension);
}
this._extensionsLoaded();
} catch (e) {
if (e instanceof Gio.DBusError) {
console.log(`Failed to connect to shell proxy: ${e}`);
this._mainStack.visible_child_name = 'noshell';
} else {
throw e;
}
}
action.set_state(new GLib.Variant('b', userExtensionsEnabled));
}
_addExtensionRow(extension) {
const row = new ExtensionRow(extension);
if (row.type === ExtensionType.PER_USER)
if (extension.isUser)
this._userList.append(row);
else
this._systemList.append(row);
this._syncListVisibility();
}
_queueUpdatesCheck() {
if (this._updatesCheckId)
return;
this._updatesCheckId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT, 1, () => {
this._checkUpdates();
this._updatesCheckId = 0;
return GLib.SOURCE_REMOVE;
});
_removeExtensionRow(extension) {
const row = this._findExtensionRow(extension);
if (row)
row.get_parent().remove(row);
this._syncListVisibility();
}
_syncListVisibility() {
this._userGroup.visible = [...this._userList].length > 1;
this._systemGroup.visible = [...this._systemList].length > 1;
if (this._userGroup.visible || this._systemGroup.visible)
this._syncVisiblePage();
}
_syncVisiblePage() {
const {extensionManager} = this.application;
if (extensionManager.failed)
this._mainStack.visible_child_name = 'noshell';
else if (this._userGroup.visible || this._systemGroup.visible)
this._mainStack.visible_child_name = 'main';
else
this._mainStack.visible_child_name = 'placeholder';
}
_checkUpdates() {
const nUpdates = [...this._userList].filter(c => c.hasUpdate).length;
const {nUpdates} = this.application.extensionManager;
this._updatesBanner.title = Gettext.ngettext(
'%d extension will be updated on next login.',