Files
Chris Monahan 87d2c6c1b8
Build Release Asset / Upload Release Asset (push) Has been cancelled
fixes prefs
2026-08-16 10:08:45 -04:00

935 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Adw from 'gi://Adw';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gdk from 'gi://Gdk';
import Gtk from 'gi://Gtk';
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
import {sensorCatalog, colorPageForSensor} from './helpers/catalog.js';
import {
compareColorEntries,
DEFAULT_THRESHOLD_RGBA,
formatColorEntry,
labelFromSensorKey,
sanitizeAndSortColorEntries,
sensorKeyBelongsToColorPage,
sensorKeyFromTypeLabel,
} from './helpers/colors.js';
import * as SensorsModule from './sensors.js';
const SENSOR_DISCOVERY_SETTLE_SECONDS = 2;
class Settings {
constructor(extensionObject) {
this._extensionObject = extensionObject;
this._settings = extensionObject.getSettings();
this._apply_icon_style();
this.builder = new Gtk.Builder();
this.builder.set_translation_domain(this._extensionObject.metadata['gettext-domain']);
GObject.type_ensure(Adw.HeaderBar.$gtype);
GObject.type_ensure(Adw.NavigationPage.$gtype);
GObject.type_ensure(Adw.NavigationSplitView.$gtype);
GObject.type_ensure(Adw.ToolbarView.$gtype);
GObject.type_ensure(Adw.ViewStack.$gtype);
this.builder.add_from_file(this._extensionObject.path + '/prefs.ui');
// Threshold color editors are built lazily when each page is first shown,
// so we do not construct dozens of Gtk.ColorButtons up front.
// pageName -> { settingsKey, page, addGroup, palettes, addColorsDropdown, ... }
this._thresholdColorPages = {};
// Session-only stash of panel sensors removed when a group is toggled off,
// so turning the group back on before closing prefs can restore them.
this._removedHotSensors = {};
// pageName -> sorted list of discovered sensor keys (from Sensors.query).
this._discoveredSensorsByPage = {};
this._sensors = null;
this._sensorDiscoveryTimeoutId = 0;
this._bind_sensor_page_gates();
this._bind_settings();
this._start_sensor_discovery();
}
_flatButton({icon_name, label, tooltip_text}) {
let props = {
valign: Gtk.Align.CENTER,
css_classes: ['flat'],
};
if (icon_name)
props.icon_name = icon_name;
if (label)
props.label = label;
if (tooltip_text)
props.tooltip_text = tooltip_text;
return new Gtk.Button(props);
}
destroy() {
if (this._sensorDiscoveryTimeoutId) {
GLib.source_remove(this._sensorDiscoveryTimeoutId);
this._sensorDiscoveryTimeoutId = 0;
}
if (this._sensors) {
this._sensors.destroy();
this._sensors = null;
}
}
_start_sensor_discovery() {
if (this._sensors)
return;
this._sensors = new SensorsModule.Sensors(this._settings, sensorCatalog, _);
let collect = (label, value, type, format) => {
this._collect_discovered_sensor(label, type, format);
};
// First pass starts hwmon discovery; second pass fills processor deltas.
this._sensors.query(collect, 1);
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
if (this._sensors)
this._sensors.query(collect, 1);
return GLib.SOURCE_REMOVE;
});
this._sensorDiscoveryTimeoutId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT, SENSOR_DISCOVERY_SETTLE_SECONDS, () => {
this._sensorDiscoveryTimeoutId = 0;
this._refresh_add_colors_dropdowns();
return GLib.SOURCE_REMOVE;
});
}
_collect_discovered_sensor(label, type, format) {
if (!type || type.endsWith('-group'))
return;
let key = sensorKeyFromTypeLabel(type, label);
if (!key || key.startsWith('__'))
return;
let pageName = colorPageForSensor(type, format);
if (!pageName)
return;
if (!this._discoveredSensorsByPage[pageName])
this._discoveredSensorsByPage[pageName] = [];
if (!this._discoveredSensorsByPage[pageName].includes(key))
this._discoveredSensorsByPage[pageName].push(key);
}
_refresh_add_colors_dropdowns() {
for (let pageName of Object.keys(this._discoveredSensorsByPage))
this._discoveredSensorsByPage[pageName].sort();
for (let pageName of Object.keys(this._thresholdColorPages))
this._sync_add_colors_row(pageName);
}
_apply_icon_style() {
let iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
let styles = ['original', 'gnome'];
let dir = styles[this._settings.get_int('icon-style')] || 'original';
let vitalsPath = `${this._extensionObject.path}/icons/${dir}`;
let iconsRoot = `${this._extensionObject.path}/icons/`;
let others = (iconTheme.get_search_path() || []).filter(p => !p.startsWith(iconsRoot));
iconTheme.set_search_path([vitalsPath].concat(others));
return iconTheme;
}
_connect_icon_style_refresh(refresh) {
this.builder.get_object('icon-style').connect('changed', refresh);
let styleManager = Adw.StyleManager.get_default();
styleManager.connect('notify::dark', refresh);
styleManager.connect('notify::color-scheme', refresh);
}
// ListBox rows use icon-name so GTK can recolor with the theme fg after
// icon-style / dark changes. set_from_paintable often leaves #bebebe unmapped.
refresh_sidebar_icons() {
this._apply_icon_style();
for (let name of Object.keys(sensorCatalog)) {
let image = this.builder.get_object('sidebar-icon-' + name);
if (!image)
continue;
let iconName = image.icon_name;
image.clear();
image.set_from_icon_name(iconName);
}
}
ensure_threshold_colors_for_page(pageName) {
if (this._thresholdColorPages[pageName] || !sensorCatalog[pageName]?.colorFormats)
return;
this._add_threshold_colors_group(
`${pageName}-page`,
`${pageName}-colors`,
pageName);
}
_action_row_for(widget) {
let current = widget;
while (current) {
if (current instanceof Adw.ActionRow)
return current;
current = current.get_parent();
}
return widget;
}
_set_dependent_widgets_sensitive(widgetIds, sensitive) {
for (let id of widgetIds) {
let widget = this.builder.get_object(id);
if (!widget)
continue;
this._action_row_for(widget).set_sensitive(sensitive);
}
}
_sync_sensor_page_sensitivity(pageName) {
let gate = this._sensorPageGates[pageName];
if (!gate)
return;
let enabled = this.builder.get_object(gate.toggle).get_active();
this._set_dependent_widgets_sensitive(gate.widgets, enabled);
let state = this._thresholdColorPages[pageName];
if (state) {
for (let group of [state.addGroup, ...state.palettes.map(p => p.group)])
group.set_sensitive(enabled);
}
if (gate.afterSync)
gate.afterSync(enabled);
}
_bind_sensor_page_gates() {
let providerWidget = this.builder.get_object('network-public-ip-provider');
let flagWidget = this.builder.get_object('network-public-ip-show-flag');
this._sensorPageGates = {
'temperature': { toggle: 'show-temperature', widgets: ['unit'] },
'voltage': { toggle: 'show-voltage', widgets: [] },
'fan': { toggle: 'show-fan', widgets: [] },
'memory': { toggle: 'show-memory', widgets: ['memory-measurement'] },
'processor': { toggle: 'show-processor', widgets: ['include-static-info'] },
'system': { toggle: 'show-system', widgets: ['monitor-cmd'] },
'network': {
toggle: 'show-network',
widgets: [
'include-public-ip',
'network-public-ip-interval',
'network-public-ip-provider',
'network-public-ip-show-flag',
'network-speed-format',
'network-speed-unit',
],
afterSync: (networkEnabled) => {
// Keep flag disabled for ipify even when network monitoring is on.
this._action_row_for(flagWidget).set_sensitive(
networkEnabled && providerWidget.get_active() !== 2);
},
},
'storage': { toggle: 'show-storage', widgets: ['storage-path', 'storage-measurement'] },
'battery': { toggle: 'show-battery', widgets: ['battery-slot'] },
'gpu': { toggle: 'show-gpu', widgets: ['include-static-gpu-info'] },
};
for (let pageName in this._sensorPageGates) {
let gate = this._sensorPageGates[pageName];
let toggle = this.builder.get_object(gate.toggle);
toggle.connect('notify::active', () => {
this._sync_sensor_page_sensitivity(pageName);
});
this._sync_sensor_page_sensitivity(pageName);
}
}
// Drop panel-pinned sensors that belong to a disabled sensor group.
// Keys look like _memory_usage_ / __network-rx_max__; group name is in the key.
_remove_hot_sensors_for_group(group) {
let hotSensors = this._settings.get_strv('hot-sensors');
let removed = [];
let filtered = hotSensors.filter(key => {
if (key === '_default_icon_')
return true;
if (key.includes(group)) {
removed.push(key);
return false;
}
return true;
});
if (removed.length === 0)
return;
this._removedHotSensors[group] = removed;
if (filtered.length === 0)
filtered.push('_default_icon_');
this._settings.set_strv('hot-sensors', filtered);
}
// Restore sensors stashed earlier in this prefs session when the group is re-enabled.
_restore_hot_sensors_for_group(group) {
let restored = this._removedHotSensors[group];
if (!restored || restored.length === 0)
return;
delete this._removedHotSensors[group];
let hotSensors = this._settings.get_strv('hot-sensors').filter(
key => key !== '_default_icon_'
);
for (let key of restored) {
if (!hotSensors.includes(key))
hotSensors.push(key);
}
this._settings.set_strv('hot-sensors', hotSensors);
}
// Bind the gtk window to the schema settings
_bind_settings() {
let widget;
// process sensor toggles
let sensors = [ 'show-temperature', 'show-voltage', 'show-fan',
'show-memory', 'show-processor', 'show-system',
'show-network', 'show-storage', 'use-higher-precision',
'alphabetize', 'hide-zeros', 'include-public-ip',
'network-public-ip-show-flag', 'show-battery', 'fixed-widths',
'hide-icons', 'menu-centered', 'include-static-info',
'show-gpu', 'include-static-gpu-info' ];
for (let key in sensors) {
let sensor = sensors[key];
widget = this.builder.get_object(sensor);
widget.set_active(this._settings.get_boolean(sensor));
widget.connect('state-set', (_, val) => {
// update hot-sensors before flipping show-* so the extension redraw sees it
if (sensor.startsWith('show-')) {
let group = sensor.substring(5);
if (!val)
this._remove_hot_sensors_for_group(group);
else
this._restore_hot_sensors_for_group(group);
}
this._settings.set_boolean(sensor, val);
});
}
// process individual drop down sensor preferences
sensors = [
'position-in-panel', 'unit', 'network-speed-format', 'network-speed-unit',
'memory-measurement', 'storage-measurement', 'battery-slot', 'icon-style',
'network-public-ip-provider'
];
for (let key in sensors) {
let sensor = sensors[key];
widget = this.builder.get_object(sensor);
widget.set_active(this._settings.get_int(sensor));
widget.connect('changed', (widget) => {
this._settings.set_int(sensor, widget.get_active());
});
}
let providerWidget = this.builder.get_object('network-public-ip-provider');
providerWidget.connect('changed', () => {
this._sync_sensor_page_sensitivity('network');
});
let updateTime = this.builder.get_object('update-time');
updateTime.set_value(this._settings.get_int('update-time'));
updateTime.connect('value-changed', (widget) => {
this._settings.set_int('update-time', Math.round(widget.get_value()));
});
this._settings.bind('network-public-ip-interval', this.builder.get_object('network-public-ip-interval'),
'value', Gio.SettingsBindFlags.DEFAULT);
// process individual text entry sensor preferences
sensors = [ 'storage-path', 'monitor-cmd' ];
for (let key in sensors) {
let sensor = sensors[key];
widget = this.builder.get_object(sensor);
widget.set_text(this._settings.get_string(sensor));
widget.connect('changed', (widget) => {
let text = widget.get_text();
if (!text) text = widget.get_placeholder_text();
this._settings.set_string(sensor, text);
});
}
}
// Runtime matching is `value >= threshold` (see values.js), so each band is
// [low, high). Integer breakpoints use high-1 in the label (039, 4059, …).
// Float breakpoints keep an explicit half-open label (0 <0.5).
_band_title(low, high) {
if (high === null || high === undefined)
return _('%s and above').format(low);
if (!(high > low))
return `${low}`;
if (Number.isInteger(low) && Number.isInteger(high))
return `${low} ${high - 1}`;
return `${low} <${high}`;
}
_threshold_for_row(row) {
let text = row._thresholdEntry.text.trim();
let value = Number.parseFloat(text);
if (text === '' || !Number.isFinite(value))
return row._committedThreshold;
return value;
}
_commit_threshold_entry(row) {
let text = row._thresholdEntry.text.trim();
let value = Number.parseFloat(text);
if (text === '' || !Number.isFinite(value)) {
row._thresholdEntry.text = `${row._committedThreshold}`;
return row._committedThreshold;
}
row._committedThreshold = value;
return value;
}
// Band pairing stays on committed order so mid-edit typing does not move
// "and above" between rows; label numbers use the live entry text.
_refresh_band_titles(rows) {
let items = rows.map(row => ({
row: row,
orderKey: row._committedThreshold,
value: this._threshold_for_row(row),
}));
items.sort((a, b) => a.orderKey - b.orderKey || a.value - b.value);
for (let i = 0; i < items.length; i++) {
let low = items[i].value;
let high = (i < items.length - 1) ? items[i + 1].value : null;
// ActionRow titles are Pango markup; unescaped `<` in float labels
// (e.g. "0 <0.5") fails to apply and leaves the old "and above" title.
items[i].row.set_title(
GLib.markup_escape_text(this._band_title(low, high), -1));
}
}
_reorder_threshold_rows(palette) {
let rows = palette.rows;
let sorted = rows.slice().sort(
(a, b) => a._committedThreshold - b._committedThreshold);
if (sorted.every((row, i) => row === rows[i]))
return;
for (let row of rows)
palette.group.remove(row);
rows.length = 0;
for (let row of sorted) {
palette.group.add(row);
rows.push(row);
}
}
_sync_all_threshold_colors(pageName) {
let state = this._thresholdColorPages[pageName];
if (!state)
return;
let items = [];
for (let palette of state.palettes) {
for (let row of palette.rows) {
let rgba = row._colorButton.get_rgba();
items.push({
threshold: this._threshold_for_row(row),
red: rgba.red,
green: rgba.green,
blue: rgba.blue,
sensorKey: palette.sensorKey || null,
});
}
}
items.sort(compareColorEntries);
this._settings.set_strv(state.settingsKey, items.map(entry => formatColorEntry(entry)));
for (let palette of state.palettes) {
this._reorder_threshold_rows(palette);
this._refresh_band_titles(palette.rows);
}
this._sync_add_colors_row(pageName);
}
_color_sensor_options(pageName, settingsKey, excludeKeys = null) {
let excluded = new Set(excludeKeys || []);
let live = [];
let liveSet = new Set();
for (let key of (this._discoveredSensorsByPage[pageName] || [])) {
if (excluded.has(key) || liveSet.has(key))
continue;
liveSet.add(key);
live.push(key);
}
for (let key of this._settings.get_strv('hot-sensors')) {
if (!sensorKeyBelongsToColorPage(pageName, key) || excluded.has(key) || liveSet.has(key))
continue;
liveSet.add(key);
live.push(key);
}
live.sort();
let orphans = [];
let orphanSet = new Set();
for (let entry of sanitizeAndSortColorEntries(this._settings.get_strv(settingsKey))) {
if (!entry.sensorKey || excluded.has(entry.sensorKey) ||
liveSet.has(entry.sensorKey) || orphanSet.has(entry.sensorKey))
continue;
orphanSet.add(entry.sensorKey);
orphans.push(entry.sensorKey);
}
orphans.sort();
return {live, orphans};
}
_palette_targets_in_use(pageName) {
let state = this._thresholdColorPages[pageName];
if (!state)
return [];
return state.palettes.map(palette => palette.sensorKey || null);
}
_sensor_key_is_live(pageName, sensorKey) {
if ((this._discoveredSensorsByPage[pageName] || []).includes(sensorKey))
return true;
return this._settings.get_strv('hot-sensors').includes(sensorKey);
}
_available_add_color_targets(pageName, settingsKey) {
let inUse = this._palette_targets_in_use(pageName);
let excludeSensors = inUse.filter(key => key !== null);
let {live, orphans} = this._color_sensor_options(pageName, settingsKey, excludeSensors);
let keys = [];
let labels = [];
if (!inUse.some(key => key === null)) {
keys.push(null);
labels.push(_('All sensors'));
}
for (let key of live) {
keys.push(key);
labels.push(labelFromSensorKey(key));
}
for (let key of orphans) {
keys.push(key);
labels.push(_('%s (unavailable)').format(labelFromSensorKey(key)));
}
return {keys, labels};
}
_populate_add_colors_dropdown_model(dropdown, pageName, settingsKey) {
let {keys, labels} = this._available_add_color_targets(pageName, settingsKey);
let model = new Gtk.StringList();
for (let label of labels)
model.append(label);
dropdown.set_model(model);
dropdown._sensorKeys = keys;
if (keys.length > 0)
dropdown.set_selected(0);
}
_sync_add_colors_row(pageName) {
let state = this._thresholdColorPages[pageName];
if (!state || !state.addColorsRow || !state.addGroup)
return;
let {keys} = this._available_add_color_targets(pageName, state.settingsKey);
let row = state.addColorsRow;
// Title belongs on this group only when it is the sole Threshold Colors
// block (no scales yet); otherwise scales carry their own titles.
state.addGroup.set_title(
state.palettes.length === 0 ? _('Threshold Colors') : '');
if (keys.length === 0) {
if (row.get_parent())
state.addGroup.remove(row);
if (state.addGroup.get_parent())
state.page.remove(state.addGroup);
return;
}
this._populate_add_colors_dropdown_model(
state.addColorsDropdown, pageName, state.settingsKey);
if (!row.get_parent())
state.addGroup.add(row);
if (!state.addGroup.get_parent()) {
state.page.add(state.addGroup);
} else {
state.page.remove(state.addGroup);
state.page.add(state.addGroup);
}
}
_palette_title(sensorKey, pageName) {
if (!sensorKey)
return _('All sensors colors');
let label = labelFromSensorKey(sensorKey);
if (this._sensor_key_is_live(pageName, sensorKey))
return _('%s colors').format(label);
return _('%s colors (unavailable)').format(label);
}
// New breakpoints sort after the current max so they become the new
// "and above" row. Prefer a whole-number step to avoid decimal creep.
_next_breakpoint_threshold(palette) {
if (!palette.rows.length)
return 0;
let values = palette.rows.map(row => row._committedThreshold)
.filter(value => Number.isFinite(value));
let max = Math.max(...values);
if (values.every(value => Number.isInteger(value)))
return max + 10;
return max + 1;
}
_make_color_row(pageName, palette, text = '0.0',
red = DEFAULT_THRESHOLD_RGBA.red,
green = DEFAULT_THRESHOLD_RGBA.green,
blue = DEFAULT_THRESHOLD_RGBA.blue,
refreshTitles = true) {
let initial = Number.parseFloat(text);
if (!Number.isFinite(initial))
initial = 0;
let entry = new Gtk.Entry({
input_purpose: Gtk.InputPurpose.NUMBER,
text: `${initial}`,
width_chars: 7,
valign: Gtk.Align.CENTER,
tooltip_text: _('Color applies at this value and up to the next breakpoint'),
});
let colorButton = new Gtk.ColorButton({
rgba: new Gdk.RGBA({red, green, blue, alpha: 1.0}),
valign: Gtk.Align.CENTER,
tooltip_text: _('Band color'),
});
let deleteButton = this._flatButton({
icon_name: 'edit-delete-symbolic',
tooltip_text: _('Remove breakpoint'),
});
let row = new Adw.ActionRow({
title: '',
activatable: false,
});
row._thresholdEntry = entry;
row._colorButton = colorButton;
row._committedThreshold = initial;
row.add_suffix(entry);
row.add_suffix(colorButton);
row.add_suffix(deleteButton);
palette.group.add(row);
palette.rows.push(row);
if (refreshTitles)
this._refresh_band_titles(palette.rows);
let commitAndSync = () => {
this._commit_threshold_entry(row);
this._sync_all_threshold_colors(pageName);
};
entry.connect('changed', () => {
this._refresh_band_titles(palette.rows);
});
entry.connect('activate', commitAndSync);
entry.connect('notify::has-focus', () => {
if (!entry.has_focus)
commitAndSync();
});
colorButton.connect('color-set', () => {
this._sync_all_threshold_colors(pageName);
});
deleteButton.connect('clicked', () => {
let index = palette.rows.indexOf(row);
if (index < 0)
return;
palette.group.remove(row);
palette.rows.splice(index, 1);
if (palette.rows.length === 0)
this._remove_threshold_palette(pageName, palette);
else
this._sync_all_threshold_colors(pageName);
});
}
_add_threshold_palette(pageName, sensorKey, entries, options = null) {
let seedDefault = !!(options && options.seedDefault);
let state = this._thresholdColorPages[pageName];
let targetKey = sensorKey || null;
let group = new Adw.PreferencesGroup({
title: this._palette_title(targetKey, pageName),
margin_start: 10,
margin_end: 10,
});
let palette = {
sensorKey: targetKey,
group,
rows: [],
};
let addButton = this._flatButton({
icon_name: 'list-add-symbolic',
tooltip_text: _('Add breakpoint'),
});
addButton.connect('clicked', () => {
this._make_color_row(
pageName, palette, `${this._next_breakpoint_threshold(palette)}`);
this._sync_all_threshold_colors(pageName);
});
let removeButton = this._flatButton({
icon_name: 'user-trash-symbolic',
tooltip_text: _('Remove color scale'),
});
removeButton.connect('clicked', () => {
this._remove_threshold_palette(pageName, palette);
});
let actions = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 0,
valign: Gtk.Align.CENTER,
});
actions.append(addButton);
actions.append(removeButton);
group.set_header_suffix(actions);
state.page.add(group);
if (state.addGroup.get_parent()) {
state.page.remove(state.addGroup);
state.page.add(state.addGroup);
}
state.palettes.push(palette);
this._sync_add_colors_row(pageName);
let rows = entries;
if (seedDefault && rows.length === 0)
rows = [{threshold: 0, ...DEFAULT_THRESHOLD_RGBA}];
for (let entry of rows) {
this._make_color_row(
pageName, palette,
`${entry.threshold}`, entry.red, entry.green, entry.blue,
false);
}
this._refresh_band_titles(palette.rows);
return palette;
}
_remove_threshold_palette(pageName, palette) {
let state = this._thresholdColorPages[pageName];
if (!state)
return;
let index = state.palettes.indexOf(palette);
if (index < 0)
return;
state.page.remove(palette.group);
state.palettes.splice(index, 1);
this._sync_all_threshold_colors(pageName);
}
_add_threshold_colors_group(pageId, settingsKey, pageName) {
let page = this.builder.get_object(pageId);
let sorted = sanitizeAndSortColorEntries(this._settings.get_strv(settingsKey));
this._settings.set_strv(settingsKey, sorted.map(entry => formatColorEntry(entry)));
let byKey = new Map();
for (let entry of sorted) {
let key = entry.sensorKey || null;
if (!byKey.has(key))
byKey.set(key, []);
byKey.get(key).push(entry);
}
let addGroup = new Adw.PreferencesGroup({
title: _('Threshold Colors'),
margin_start: 10,
margin_end: 10,
});
let state = {
settingsKey,
page,
addGroup,
palettes: [],
addColorsDropdown: null,
addColorsRow: null,
};
this._thresholdColorPages[pageName] = state;
if (byKey.has(null))
this._add_threshold_palette(pageName, null, byKey.get(null));
let sensorKeys = [...byKey.keys()].filter(key => key !== null).sort();
for (let key of sensorKeys)
this._add_threshold_palette(pageName, key, byKey.get(key) || []);
let addColorsDropdown = new Gtk.DropDown({
valign: Gtk.Align.CENTER,
hexpand: true,
tooltip_text: _('Apply a color scale to all sensors or one sensor'),
});
this._populate_add_colors_dropdown_model(addColorsDropdown, pageName, settingsKey);
let addColorsButton = this._flatButton({
icon_name: 'list-add-symbolic',
label: _('Add'),
});
let addColorsRow = new Adw.ActionRow({
title: _('Add Color Scale'),
activatable_widget: addColorsButton,
});
addColorsRow.add_suffix(addColorsDropdown);
addColorsRow.add_suffix(addColorsButton);
addGroup.add(addColorsRow);
page.add(addGroup);
state.addColorsDropdown = addColorsDropdown;
state.addColorsRow = addColorsRow;
this._sync_add_colors_row(pageName);
addColorsButton.connect('clicked', () => {
let selected = addColorsDropdown.get_selected();
let keys = addColorsDropdown._sensorKeys || [];
if (selected >= keys.length)
return;
let sensorKey = keys[selected];
if (sensorKey === undefined)
return;
let target = sensorKey || null;
if (state.palettes.some(palette => (palette.sensorKey || null) === target))
return;
this._add_threshold_palette(pageName, sensorKey, [], {seedDefault: true});
this._sync_all_threshold_colors(pageName);
this._sync_sensor_page_sensitivity(pageName);
});
this._sync_sensor_page_sensitivity(pageName);
}
}
export default class VitalsPrefs extends ExtensionPreferences {
fillPreferencesWindow(window) {
window._settings = this.getSettings();
let settings = new Settings(this);
window._vitalsSettings = settings;
window.connect('close-request', () => {
settings.destroy();
window._vitalsSettings = null;
});
window.set_search_enabled(false);
window.set_default_size(720, 620);
let root = settings.builder.get_object('prefs-root');
let stack = settings.builder.get_object('prefs-stack');
let list = settings.builder.get_object('prefs-sidebar');
let contentPage = settings.builder.get_object('prefs-content-page');
// Replace PreferencesWindow's bottom-tab navigation with the sidebar shell.
window.get_content().set_child(root);
let pages = [{ name: 'general' }];
for (let name of Object.keys(sensorCatalog))
pages.push({ name });
for (let info of pages) {
let page = settings.builder.get_object(info.name + '-page');
let title = page.get_title();
let iconName = page.get_icon_name();
// Header bar already shows the section title; hide the page banner.
page.set_title('');
stack.add_titled_with_icon(page, info.name, title, iconName);
}
let selectRowForVisible = () => {
let name = stack.get_visible_child_name();
for (let i = 0; ; i++) {
let row = list.get_row_at_index(i);
if (!row)
break;
if (row.name === name) {
list.select_row(row);
break;
}
}
};
list.connect('row-activated', (_list, row) => {
if (!row?.name || !stack.get_child_by_name(row.name))
return;
stack.set_visible_child_name(row.name);
root.set_show_content(true);
});
list.connect('row-selected', (_list, row) => {
if (!row?.name || !stack.get_child_by_name(row.name))
return;
if (stack.get_visible_child_name() !== row.name)
stack.set_visible_child_name(row.name);
});
let syncVisiblePage = () => {
let name = stack.get_visible_child_name();
selectRowForVisible();
let visible = stack.get_visible_child();
if (visible) {
let title = stack.get_page(visible).get_title();
if (name && name !== 'general')
title = title + ' ' + _('Preferences');
contentPage.set_title(title);
}
if (name)
settings.ensure_threshold_colors_for_page(name);
};
stack.connect('notify::visible-child', syncVisiblePage);
syncVisiblePage();
settings.refresh_sidebar_icons();
settings._connect_icon_style_refresh(() => {
settings.refresh_sidebar_icons();
});
// GNOME Shell's prefs host requires visible_page after fillPreferencesWindow
// (extensionPrefsDialog.js). The sidebar replaces the window content, so
// register an unused page to satisfy that check.
window.add(new Adw.PreferencesPage());
}
}