extensions-app: Use ListModel to track extensions

Manually adding and removing rows to dynamic lists is rather
old-fashioned, GTK 4 strongly encourages the use of models.

Modernize the code by exposing extensions as ListModel, and
bind it to the two lists with appropriate filters.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3067>
This commit is contained in:
Florian Müllner 2023-12-19 18:57:37 +01:00 committed by Marge Bot
parent 2c592059bc
commit 7907b9754b
3 changed files with 89 additions and 105 deletions

View File

@ -12,6 +12,43 @@
</item> </item>
</section> </section>
</menu> </menu>
<object class="GtkSortListModel" id="sortModel">
<property name="sorter">
<object class="GtkStringSorter">
<property name="expression">
<lookup name="name" type="Extension"/>
</property>
</object>
</property>
</object>
<object class="GtkStringFilter" id="searchFilter">
<property name="ignore-case">true</property>
<property name="match-mode">substring</property>
<property name="expression">
<lookup name="name" type="Extension"/>
</property>
</object>
<object class="GtkFilterListModel" id="userListModel">
<property name="model">sortModel</property>
<property name="filter">
<object class="GtkBoolFilter">
<property name="expression">
<lookup name="is-user" type="Extension"/>
</property>
</object>
</property>
</object>
<object class="GtkFilterListModel" id="systemListModel">
<property name="model">sortModel</property>
<property name="filter">
<object class="GtkBoolFilter">
<property name="invert">true</property>
<property name="expression">
<lookup name="is-user" type="Extension"/>
</property>
</object>
</property>
</object>
<template class="ExtensionsWindow" parent="AdwApplicationWindow"> <template class="ExtensionsWindow" parent="AdwApplicationWindow">
<property name="default-width">800</property> <property name="default-width">800</property>
<property name="default-height">500</property> <property name="default-height">500</property>
@ -139,6 +176,10 @@
<child> <child>
<object class="AdwPreferencesGroup" id="userGroup"> <object class="AdwPreferencesGroup" id="userGroup">
<property name="title" translatable="yes">User Extensions</property> <property name="title" translatable="yes">User Extensions</property>
<property name="visible"
bind-source="userListModel"
bind-property="n-items"
bind-flags="sync-create"/>
<child> <child>
<object class="GtkListBox" id="userList"> <object class="GtkListBox" id="userList">
<property name="selection-mode">none</property> <property name="selection-mode">none</property>
@ -155,6 +196,10 @@
<child> <child>
<object class="AdwPreferencesGroup" id="systemGroup"> <object class="AdwPreferencesGroup" id="systemGroup">
<property name="title" translatable="yes">System Extensions</property> <property name="title" translatable="yes">System Extensions</property>
<property name="visible"
bind-source="systemListModel"
bind-property="n-items"
bind-flags="sync-create"/>
<child> <child>
<object class="GtkListBox" id="systemList"> <object class="GtkListBox" id="systemList">
<property name="selection-mode">none</property> <property name="selection-mode">none</property>

View File

@ -108,8 +108,6 @@ const Extension = GObject.registerClass({
const {name} = metadata; const {name} = metadata;
if (this._name !== name) { if (this._name !== name) {
[this._keywords] = GLib.str_tokenize_and_fold(name, null);
this._name = name; this._name = name;
this.notify('name'); this.notify('name');
} }
@ -207,10 +205,6 @@ const Extension = GObject.registerClass({
return this._version; return this._version;
} }
get keywords() {
return this._keywords;
}
get error() { get error() {
if (!this.hasError) if (!this.hasError)
return ''; return '';
@ -268,6 +262,10 @@ export const ExtensionManager = GObject.registerClass({
'user-extensions-enabled', null, null, 'user-extensions-enabled', null, null,
GObject.ParamFlags.READWRITE, GObject.ParamFlags.READWRITE,
true), true),
'extensions': GObject.ParamSpec.object(
'extensions', null, null,
GObject.ParamFlags.READABLE,
Gio.ListModel),
'n-updates': GObject.ParamSpec.int( 'n-updates': GObject.ParamSpec.int(
'n-updates', null, null, 'n-updates', null, null,
GObject.ParamFlags.READABLE, GObject.ParamFlags.READABLE,
@ -278,16 +276,13 @@ export const ExtensionManager = GObject.registerClass({
false), false),
}, },
Signals: { Signals: {
'extension-added': {param_types: [Extension]},
'extension-removed': {param_types: [Extension]},
'extension-changed': {param_types: [Extension], flags: GObject.SignalFlags.DETAILED},
'extensions-loaded': {}, 'extensions-loaded': {},
}, },
}, class ExtensionManager extends GObject.Object { }, class ExtensionManager extends GObject.Object {
constructor() { constructor() {
super(); super();
this._extensions = new Map(); this._extensions = new Gio.ListStore({itemType: Extension});
this._proxyReady = false; this._proxyReady = false;
this._shellProxy = new GnomeShellProxy(Gio.DBus.session, this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
@ -312,6 +307,10 @@ export const ExtensionManager = GObject.registerClass({
this._loadExtensions().catch(console.error); this._loadExtensions().catch(console.error);
} }
get extensions() {
return this._extensions;
}
get userExtensionsEnabled() { get userExtensionsEnabled() {
return this._shellProxy.UserExtensionsEnabled ?? false; return this._shellProxy.UserExtensionsEnabled ?? false;
} }
@ -322,7 +321,7 @@ export const ExtensionManager = GObject.registerClass({
get nUpdates() { get nUpdates() {
let nUpdates = 0; let nUpdates = 0;
for (const ext of this._extensions.values()) { for (const ext of this._extensions) {
if (ext.isUser && ext.hasUpdate) if (ext.isUser && ext.hasUpdate)
nUpdates++; nUpdates++;
} }
@ -355,43 +354,37 @@ export const ExtensionManager = GObject.registerClass({
this._shellProxy.CheckForUpdatesAsync().catch(console.error); this._shellProxy.CheckForUpdatesAsync().catch(console.error);
} }
_addExtension(extension) {
const {uuid} = extension;
if (this._extensions.has(uuid))
return;
this._extensions.set(uuid, extension);
this.emit('extension-added', extension);
}
_removeExtension(extension) {
const {uuid} = extension;
if (this._extensions.delete(uuid))
this.emit('extension-removed', extension);
}
async _loadExtensions() { async _loadExtensions() {
const [extensionsMap] = await this._shellProxy.ListExtensionsAsync(); const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
for (let uuid in extensionsMap) { for (let uuid in extensionsMap) {
const extension = new Extension(extensionsMap[uuid]); const extension = new Extension(extensionsMap[uuid]);
this._addExtension(extension); this._extensions.append(extension);
} }
this.emit('extensions-loaded'); this.emit('extensions-loaded');
} }
_findExtension(uuid) {
const len = this._extensions.get_n_items();
for (let pos = 0; pos < len; pos++) {
const extension = this._extensions.get_item(pos);
if (extension.uuid === uuid)
return [extension, pos];
}
return [null, -1];
}
_onExtensionStateChanged(p, sender, [uuid, newState]) { _onExtensionStateChanged(p, sender, [uuid, newState]) {
const extension = this._extensions.get(uuid); const [extension, pos] = this._findExtension(uuid);
if (extension) if (extension)
extension.update(newState); extension.update(newState);
if (!extension) if (!extension)
this._addExtension(new Extension(newState)); this._extensions.append(new Extension(newState));
else if (extension.state === ExtensionState.UNINSTALLED) else if (extension.state === ExtensionState.UNINSTALLED)
this._removeExtension(extension); this._extensions.remove(pos);
else
this.emit(`extension-changed::${uuid}`, extension);
if (this._updatesCheckId) if (this._updatesCheckId)
return; return;

View File

@ -18,6 +18,10 @@ export const ExtensionsWindow = GObject.registerClass({
GTypeName: 'ExtensionsWindow', GTypeName: 'ExtensionsWindow',
Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui', Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui',
InternalChildren: [ InternalChildren: [
'sortModel',
'searchFilter',
'userListModel',
'systemListModel',
'userGroup', 'userGroup',
'userList', 'userList',
'systemGroup', 'systemGroup',
@ -54,20 +58,9 @@ export const ExtensionsWindow = GObject.registerClass({
}, },
}]); }]);
this._searchTerms = []; this._searchEntry.connect('search-changed',
this._searchEntry.connect('search-changed', () => { () => (this._searchFilter.search = this._searchEntry.text));
const {text} = this._searchEntry;
if (text === '')
this._searchTerms = [];
else
[this._searchTerms] = GLib.str_tokenize_and_fold(text, null);
this._userList.invalidate_filter();
this._systemList.invalidate_filter();
});
this._userList.set_sort_func(this._sortList.bind(this));
this._userList.set_filter_func(this._filterList.bind(this));
this._userList.set_placeholder(new Gtk.Label({ this._userList.set_placeholder(new Gtk.Label({
label: _('No Matches'), label: _('No Matches'),
margin_start: 12, margin_start: 12,
@ -76,9 +69,8 @@ export const ExtensionsWindow = GObject.registerClass({
margin_bottom: 12, margin_bottom: 12,
})); }));
this._userList.connect('row-activated', (_list, row) => row.activate()); this._userList.connect('row-activated', (_list, row) => row.activate());
this._userGroup.connect('notify::visible', () => this._syncVisiblePage());
this._systemList.set_sort_func(this._sortList.bind(this));
this._systemList.set_filter_func(this._filterList.bind(this));
this._systemList.set_placeholder(new Gtk.Label({ this._systemList.set_placeholder(new Gtk.Label({
label: _('No Matches'), label: _('No Matches'),
margin_start: 12, margin_start: 12,
@ -87,6 +79,7 @@ export const ExtensionsWindow = GObject.registerClass({
margin_bottom: 12, margin_bottom: 12,
})); }));
this._systemList.connect('row-activated', (_list, row) => row.activate()); this._systemList.connect('row-activated', (_list, row) => row.activate());
this._systemGroup.connect('notify::visible', () => this._syncVisiblePage());
const {extensionManager} = this.application; const {extensionManager} = this.application;
extensionManager.connect('notify::failed', extensionManager.connect('notify::failed',
@ -97,19 +90,16 @@ export const ExtensionsWindow = GObject.registerClass({
this._onUserExtensionsEnabledChanged.bind(this)); this._onUserExtensionsEnabledChanged.bind(this));
this._onUserExtensionsEnabledChanged(); this._onUserExtensionsEnabledChanged();
extensionManager.connect('extension-added', this._sortModel.model = extensionManager.extensions;
(mgr, extension) => this._addExtensionRow(extension));
extensionManager.connect('extension-removed', this._userList.bind_model(new Gtk.FilterListModel({
(mgr, extension) => this._removeExtensionRow(extension)); filter: this._searchFilter,
extensionManager.connect('extension-changed', model: this._userListModel,
(mgr, extension) => { }), extension => new ExtensionRow(extension));
const row = this._findExtensionRow(extension); this._systemList.bind_model(new Gtk.FilterListModel({
const isUser = row?.get_parent() === this._userList; filter: this._searchFilter,
if (extension.isUser !== isUser) { model: this._systemListModel,
this._removeExtensionRow(extension); }), extension => new ExtensionRow(extension));
this._addExtensionRow(extension);
}
});
extensionManager.connect('extensions-loaded', extensionManager.connect('extensions-loaded',
() => this._extensionsLoaded()); () => this._extensionsLoaded());
@ -189,56 +179,12 @@ export const ExtensionsWindow = GObject.registerClass({
null); null);
} }
_sortList(row1, row2) {
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 => keywords.some(k => k.startsWith(t)));
}
_findExtensionRow(extension) {
return [
...this._userList,
...this._systemList,
].find(c => c.extension === extension);
}
_onUserExtensionsEnabledChanged() { _onUserExtensionsEnabledChanged() {
const {userExtensionsEnabled} = this.application.extensionManager; const {userExtensionsEnabled} = this.application.extensionManager;
const action = this.lookup_action('user-extensions-enabled'); const action = this.lookup_action('user-extensions-enabled');
action.set_state(new GLib.Variant('b', userExtensionsEnabled)); action.set_state(new GLib.Variant('b', userExtensionsEnabled));
} }
_addExtensionRow(extension) {
const row = new ExtensionRow(extension);
if (extension.isUser)
this._userList.append(row);
else
this._systemList.append(row);
this._syncListVisibility();
}
_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;
this._syncVisiblePage();
}
_syncVisiblePage() { _syncVisiblePage() {
const {extensionManager} = this.application; const {extensionManager} = this.application;
@ -261,7 +207,7 @@ export const ExtensionsWindow = GObject.registerClass({
} }
_extensionsLoaded() { _extensionsLoaded() {
this._syncListVisibility(); this._syncVisiblePage();
this._checkUpdates(); this._checkUpdates();
} }
}); });