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:
parent
2c592059bc
commit
7907b9754b
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user