diff --git a/extension.js b/extension.js index de33e4e..6f86758 100644 --- a/extension.js +++ b/extension.js @@ -301,6 +301,8 @@ var VitalsMenuButton = GObject.registerClass({ this._warnings = []; this._isDestroyed = false; this._sensorMenuItems = {}; + this._availableSensors = {}; + this._availableSensorsSig = ''; this._hotLabels = {}; this._hotItems = {}; this._groups = {}; @@ -343,6 +345,7 @@ var VitalsMenuButton = GObject.registerClass({ 'changed::position-in-panel', this._positionInPanelChanged.bind(this), 'changed::menu-centered', this._positionInPanelChanged.bind(this), 'changed::icon-style', this._iconStyleChanged.bind(this), + 'changed::hot-sensors', this._hotSensorsChanged.bind(this), this); let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', @@ -498,6 +501,40 @@ var VitalsMenuButton = GObject.registerClass({ )); } + _hotSensorsChanged() { + // 'hot-sensors' changed. When the change came from the dropdown the + // panel already matches, so only redraw when it actually differs from + // what is currently shown (e.g. the change came from the preferences + // sensor selector, in this same or another process). Comparing sets is + // timing-independent, unlike a flag (the 'changed' signal is async). + let desired = this._settings.get_strv('hot-sensors'); + let current = Object.keys(this._hotItems); + if (desired.length === current.length && desired.every(k => current.includes(k))) + return; + + this._redrawMenu(); + } + + // publishes the list of sensors discovered so far into a setting the + // preferences window (a separate process) can read to build its selector + _publishAvailableSensors() { + let list = []; + for (let key in this._availableSensors) { + let sensor = this._availableSensors[key]; + list.push({ key: key, label: sensor.label, group: sensor.group }); + } + + // stable ordering so the signature only changes when the set changes + list.sort((a, b) => + a.group.localeCompare(b.group) || + a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: 'base' })); + + let json = JSON.stringify(list); + if (json === this._availableSensorsSig) return; + this._availableSensorsSig = json; + this._settings.set_string('available-sensors', json); + } + _initializeTimer() { // used to query sensors and update display let update_time = this._settings.get_int('update-time'); @@ -699,6 +736,10 @@ var VitalsMenuButton = GObject.registerClass({ let icon = (split.length == 2)?'icon-' + split[1]:'icon'; let gicon = Gio.icon_new_for_string(this._sensorIconPath(type, icon)); + // remember this sensor so the preferences selector can list it + this._availableSensors[key] = { label: sensor.label, group: type }; + this._publishAvailableSensors(); + let item = new MenuItem.MenuItem(gicon, key, sensor.label, sensor.value, this._hotLabels[key]); item.connect('toggle', (self) => { let hotSensors = this._settings.get_strv('hot-sensors'); diff --git a/prefs.js b/prefs.js index 332da0c..76edbca 100644 --- a/prefs.js +++ b/prefs.js @@ -138,12 +138,141 @@ export default class VitalsPrefs extends ExtensionPreferences { let settings = new Settings(this); let widget = settings.widget; - const page = new Adw.PreferencesPage(); + const page = new Adw.PreferencesPage({ + title: _('General'), + icon_name: 'preferences-system-symbolic' + }); const group = new Adw.PreferencesGroup({}); group.add(widget); page.add(group); window.add(page); window.set_default_size(widget.width, widget.height); widget.show(); + + // second page: pick exactly which sensors show in the top bar + this._buildPanelSensorsPage(window, window._settings); + } + + // Builds a page that lists every sensor the extension has discovered, + // grouped by category, with a switch per sensor. Toggling a switch adds or + // removes that sensor from 'hot-sensors' (the sensors shown in the panel). + _buildPanelSensorsPage(window, settings) { + const page = new Adw.PreferencesPage({ + title: _('Panel'), + icon_name: 'utilities-system-monitor-symbolic' + }); + window.add(page); + + // groups we add dynamically, so we can rebuild when the sensor list changes + let dynamicGroups = []; + let keyToSwitch = {}; + // guards against feedback loops between our writes and the change signals + let updating = false; + + const rebuild = () => { + for (let g of dynamicGroups) page.remove(g); + dynamicGroups = []; + keyToSwitch = {}; + + let available = []; + try { + available = JSON.parse(settings.get_string('available-sensors')); + } catch (e) { + available = []; + } + + if (!available.length) { + let empty = new Adw.PreferencesGroup({ + title: _('Sensors in the panel'), + description: _('Open the Vitals menu in the top bar once so your sensors can be detected. They will then appear here to be toggled on or off.') + }); + page.add(empty); + dynamicGroups.push(empty); + return; + } + + let intro = new Adw.PreferencesGroup({ + title: _('Sensors in the panel'), + description: _('Choose which sensors are shown in the top bar.') + }); + page.add(intro); + dynamicGroups.push(intro); + + // bucket sensors by their category + let byGroup = {}; + for (let sensor of available) { + if (!byGroup[sensor.group]) byGroup[sensor.group] = []; + byGroup[sensor.group].push(sensor); + } + + let hot = settings.get_strv('hot-sensors'); + + for (let groupName of Object.keys(byGroup).sort()) { + let pg = new Adw.PreferencesGroup({ title: this._groupDisplayName(groupName) }); + + for (let sensor of byGroup[groupName]) { + let row = new Adw.SwitchRow({ + title: sensor.label, + active: hot.indexOf(sensor.key) >= 0 + }); + keyToSwitch[sensor.key] = row; + + row.connect('notify::active', () => { + if (updating) return; + + let list = settings.get_strv('hot-sensors'); + let idx = list.indexOf(sensor.key); + if (row.active && idx < 0) list.push(sensor.key); + else if (!row.active && idx >= 0) list.splice(idx, 1); + else return; + + // same invariant the panel uses: show the generic + // placeholder icon only when no real sensor is selected + let defIdx = list.indexOf('_default_icon_'); + if (defIdx >= 0) list.splice(defIdx, 1); + if (list.length === 0) list.push('_default_icon_'); + + updating = true; + settings.set_strv('hot-sensors', list); + updating = false; + }); + + pg.add(row); + } + + page.add(pg); + dynamicGroups.push(pg); + } + }; + + rebuild(); + + // extension discovered new sensors (or the app just started) -> rebuild + let availId = settings.connect('changed::available-sensors', () => rebuild()); + + // hot-sensors changed elsewhere (e.g. the dropdown) -> reflect in switches + let hotId = settings.connect('changed::hot-sensors', () => { + if (updating) return; + let hot = settings.get_strv('hot-sensors'); + updating = true; + for (let key in keyToSwitch) + keyToSwitch[key].active = hot.indexOf(key) >= 0; + updating = false; + }); + + // avoid leaking the signal handlers when the window is closed + window.connect('close-request', () => { + settings.disconnect(availId); + settings.disconnect(hotId); + return false; + }); + } + + _groupDisplayName(group) { + if (group.startsWith('gpu')) { + let n = group.split('#')[1]; + return n ? _('Graphics') + ' ' + n : _('Graphics'); + } + return group.charAt(0).toUpperCase() + group.slice(1); } } diff --git a/schemas/org.gnome.shell.extensions.vitals.gschema.xml b/schemas/org.gnome.shell.extensions.vitals.gschema.xml index 4f0b186..e1933fb 100644 --- a/schemas/org.gnome.shell.extensions.vitals.gschema.xml +++ b/schemas/org.gnome.shell.extensions.vitals.gschema.xml @@ -6,6 +6,11 @@ Sensors to show in panel List of sensors to be shown in the panel + + "[]" + Discovered sensors + JSON list of sensors discovered by the extension, published so the preferences window can offer a selector for which sensors appear in the panel + 5 Seconds between updates