From 739b1e11a0ee38eccf244a68431baa7a041acd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Sat, 11 Apr 2020 05:29:56 +0200 Subject: [PATCH] user-themes: Add preference widget While we don't endorse or support 3rd party theming, the extension exists and is actively used. However right now the most convenient way of setting it up is by installing Tweak Tool; give users an alternative by providing a simple settings dialog ourselves. https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/117 --- extensions/user-theme/extension.js | 14 +-- extensions/user-theme/meson.build | 1 + extensions/user-theme/prefs.js | 176 +++++++++++++++++++++++++++++ extensions/user-theme/util.js | 12 ++ 4 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 extensions/user-theme/prefs.js create mode 100644 extensions/user-theme/util.js diff --git a/extensions/user-theme/extension.js b/extensions/user-theme/extension.js index 04da83b2..dff00f88 100644 --- a/extensions/user-theme/extension.js +++ b/extensions/user-theme/extension.js @@ -2,11 +2,14 @@ // Load shell theme from ~/.local/share/themes/name/gnome-shell /* exported init */ -const { Gio, GLib } = imports.gi; +const { Gio } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; +const Me = ExtensionUtils.getCurrentExtension(); +const Util = Me.imports.util; + const SETTINGS_KEY = 'name'; class ThemeManager { @@ -34,13 +37,8 @@ class ThemeManager { let themeName = this._settings.get_string(SETTINGS_KEY); if (themeName) { - let stylesheetPaths = [ - [GLib.get_home_dir(), '.themes'], - [GLib.get_user_data_dir(), 'themes'], - ...GLib.get_system_data_dirs().map(dir => [dir, 'themes']), - ].map(themeDir => GLib.build_filenamev([ - ...themeDir, themeName, 'gnome-shell', 'gnome-shell.css', - ])); + const stylesheetPaths = Util.getThemeDirs() + .map(dir => `${dir}/${themeName}/gnome-shell/gnome-shell.css`); stylesheet = stylesheetPaths.find(path => { let file = Gio.file_new_for_path(path); diff --git a/extensions/user-theme/meson.build b/extensions/user-theme/meson.build index 585c02da..e7c66d41 100644 --- a/extensions/user-theme/meson.build +++ b/extensions/user-theme/meson.build @@ -4,4 +4,5 @@ extension_data += configure_file( configuration: metadata_conf ) +extension_sources += files('prefs.js', 'util.js') extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') diff --git a/extensions/user-theme/prefs.js b/extensions/user-theme/prefs.js new file mode 100644 index 00000000..2f52af54 --- /dev/null +++ b/extensions/user-theme/prefs.js @@ -0,0 +1,176 @@ +// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- +/* exported init buildPrefsWidget */ + +// we use async/await here to not block the mainloop, not to parallelize +/* eslint-disable no-await-in-loop */ + +const { Gio, GLib, GObject, Gtk } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; + +const Me = ExtensionUtils.getCurrentExtension(); +const Util = Me.imports.util; + +Gio._promisify(Gio._LocalFilePrototype, + 'enumerate_children_async', 'enumerate_children_finish'); +Gio._promisify(Gio._LocalFilePrototype, + 'query_info_async', 'query_info_finish'); +Gio._promisify(Gio.FileEnumerator.prototype, + 'next_files_async', 'next_files_finish'); + +const UserThemePrefsWidget = GObject.registerClass( +class UserThemePrefsWidget extends Gtk.ScrolledWindow { + _init() { + super._init({ + hscrollbar_policy: Gtk.PolicyType.NEVER, + }); + + const box = new Gtk.Box(); + this.add(box); + + this._list = new Gtk.ListBox({ + halign: Gtk.Align.CENTER, + valign: Gtk.Align.START, + hexpand: true, + margin: 60, + }); + this._list.get_style_context().add_class('frame'); + this._list.set_header_func(this._updateHeader.bind(this)); + box.add(this._list); + + this._actionGroup = new Gio.SimpleActionGroup(); + this._list.insert_action_group('theme', this._actionGroup); + + this._settings = ExtensionUtils.getSettings(); + this._actionGroup.add_action( + this._settings.create_action('name')); + + this.connect('destroy', () => this._settings.run_dispose()); + + this._rows = new Map(); + this._addTheme(''); // default + + this._collectThemes(); + } + + async _collectThemes() { + for (const dirName of Util.getThemeDirs()) { + const dir = Gio.File.new_for_path(dirName); + for (const name of await this._enumerateDir(dir)) { + if (this._rows.has(name)) + continue; + + const file = dir.resolve_relative_path( + `${name}/gnome-shell/gnome-shell.css`); + try { + await file.query_info_async( + Gio.FILE_ATTRIBUTE_STANDARD_NAME, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, null); + this._addTheme(name); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) + logError(e); + } + } + } + } + + _addTheme(name) { + const row = new ThemeRow(name); + this._rows.set(name, row); + + this._list.add(row); + row.show_all(); + } + + async _enumerateDir(dir) { + const fileInfos = []; + let fileEnum; + + try { + fileEnum = await dir.enumerate_children_async( + Gio.FILE_ATTRIBUTE_STANDARD_NAME, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, null); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) + logError(e); + return []; + } + + let infos; + do { + infos = await fileEnum.next_files_async(100, + GLib.PRIORITY_DEFAULT, null); + fileInfos.push(...infos); + } while (infos.length > 0); + + return fileInfos.map(info => info.get_name()); + } + + _updateHeader(row, before) { + if (!before || row.get_header()) + return; + row.set_header(new Gtk.Separator()); + } +}); + +const ThemeRow = GObject.registerClass( +class ThemeRow extends Gtk.ListBoxRow { + _init(name) { + this._name = new GLib.Variant('s', name); + + super._init({ + action_name: 'theme.name', + action_target: this._name, + }); + + const box = new Gtk.Box({ + spacing: 12, + margin: 12, + }); + this.add(box); + + box.add(new Gtk.Label({ + label: name || 'Default', + hexpand: true, + xalign: 0, + max_width_chars: 25, + width_chars: 25, + })); + + this._checkmark = new Gtk.Image({ + icon_name: 'emblem-ok-symbolic', + pixel_size: 16, + }); + box.add(this._checkmark); + + box.show_all(); + + const id = this.connect('parent-set', () => { + this.disconnect(id); + + const actionGroup = this.get_action_group('theme'); + actionGroup.connect('action-state-changed::name', + this._syncCheckmark.bind(this)); + this._syncCheckmark(); + }); + } + + _syncCheckmark() { + const actionGroup = this.get_action_group('theme'); + const state = actionGroup.get_action_state('name'); + this._checkmark.opacity = this._name.equal(state); + } +}); + +function init() { +} + +function buildPrefsWidget() { + let widget = new UserThemePrefsWidget(); + widget.show_all(); + + return widget; +} diff --git a/extensions/user-theme/util.js b/extensions/user-theme/util.js new file mode 100644 index 00000000..c0833b08 --- /dev/null +++ b/extensions/user-theme/util.js @@ -0,0 +1,12 @@ +/* exported getThemeDirs */ +const { GLib } = imports.gi; + +const fn = (...args) => GLib.build_filenamev(args); + +function getThemeDirs() { + return [ + fn(GLib.get_home_dir(), '.themes'), + fn(GLib.get_user_data_dir(), 'themes'), + ...GLib.get_system_data_dirs().map(dir => fn(dir, 'themes')), + ]; +}