import Adw from 'gi://Adw'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import Gtk from 'gi://Gtk'; import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; import { PROFILE_VARIANT_TYPE, ProfileIndex, getTextScalingFactor, setTextScalingFactor, } from './display-config.js'; const RULE_FIELDS = [ ['connector', 'Connector'], ['vendor', 'Vendor'], ['product', 'Product'], ['serial', 'Serial'], ]; export default class DisplayProfileSwitcherPreferences extends ExtensionPreferences { fillPreferencesWindow(window) { this._settings = this.getSettings(); this._profiles = this._settings.get_value('profiles').deepUnpack(); const page = new Adw.PreferencesPage({title: 'Display Profiles', icon_name: 'video-display-symbolic'}); const behavior = new Adw.PreferencesGroup({title: 'Behavior'}); const autoApply = new Adw.SwitchRow({ title: 'Apply automatically', subtitle: 'Apply the first matching profile shortly after displays change.', }); this._settings.bind('automatically-apply', autoApply, 'active', Gio.SettingsBindFlags.DEFAULT); behavior.add(autoApply); page.add(behavior); this._profilesGroup = new Adw.PreferencesGroup({ title: 'Saved profiles', description: 'Rules are case-insensitive JavaScript regular expressions. Every rule must match a connected monitor.', }); this._profileRows = []; page.add(this._profilesGroup); window.add(page); this._rebuildProfiles(); window.connect('close-request', () => { this._settings = null; this._profiles = null; this._profilesGroup = null; this._profileRows = null; }); } _rebuildProfiles() { for (const row of this._profileRows) this._profilesGroup.remove(row); this._profileRows = []; if (!this._profiles.length) { const row = new Adw.ActionRow({ title: 'No profiles saved', subtitle: 'Open the Displays quick-settings menu and choose “Save current layout as profile”.', }); this._profilesGroup.add(row); this._profileRows.push(row); return; } this._profiles.forEach((profile, profileIndex) => { const row = this._profileRow(profile, profileIndex); this._profilesGroup.add(row); this._profileRows.push(row); }); } _profileRow(profile, profileIndex) { const row = new Adw.ExpanderRow({title: profile[ProfileIndex.NAME], expanded: false}); const name = new Gtk.Entry({text: profile[ProfileIndex.NAME], hexpand: true, valign: Gtk.Align.CENTER}); const nameRow = new Adw.ActionRow({title: 'Name'}); nameRow.add_suffix(name); row.add_row(nameRow); name.connect('changed', entry => { this._profiles[profileIndex][ProfileIndex.NAME] = entry.text; row.title = entry.text || 'Unnamed profile'; this._saveProfiles(); }); profile[ProfileIndex.RULES].forEach((rule, ruleIndex) => { const ruleRow = new Adw.ExpanderRow({title: `Monitor rule ${ruleIndex + 1}`, expanded: false}); for (const [field, label] of RULE_FIELDS) { const entry = new Gtk.Entry({text: rule[field] ?? '', hexpand: true, valign: Gtk.Align.CENTER}); const fieldRow = new Adw.ActionRow({title: label}); fieldRow.add_suffix(entry); ruleRow.add_row(fieldRow); entry.connect('changed', widget => { this._profiles[profileIndex][ProfileIndex.RULES][ruleIndex][field] = widget.text; this._saveProfiles(); }); } row.add_row(ruleRow); }); this._addSavedLayoutRows(row, profile, profileIndex); const remove = new Gtk.Button({label: 'Remove', valign: Gtk.Align.CENTER, css_classes: ['destructive-action']}); remove.connect('clicked', () => { this._profiles.splice(profileIndex, 1); this._saveProfiles(); this._rebuildProfiles(); }); row.add_suffix(remove); return row; } _addSavedLayoutRows(row, profile, profileIndex) { const logicalMonitors = profile[ProfileIndex.LOGICAL_MONITORS]; const layoutRow = new Adw.ActionRow({ title: 'Saved display layout', subtitle: `${logicalMonitors.length} logical display${logicalMonitors.length === 1 ? '' : 's'}`, }); row.add_row(layoutRow); logicalMonitors.forEach(([x, y, scale, transform, primary, monitors], index) => { const monitorSummary = monitors.map(([connector, mode]) => `${connector}: ${mode}`).join(', '); const transformName = ['Normal', '90° clockwise', 'Upside-down', '90° counter-clockwise'][transform] ?? `Transform ${transform}`; row.add_row(new Adw.ActionRow({ title: `Display ${index + 1}${primary ? ' (primary)' : ''}`, subtitle: `${monitorSummary}; ${Math.round(scale * 100)}% display scale; ${transformName}; position ${x}, ${y}`, })); }); const savedTextScale = getTextScalingFactor(profile); const textScale = new Gtk.SpinButton({ adjustment: new Gtk.Adjustment({lower: 0.5, upper: 3.0, step_increment: 0.05, page_increment: 0.25}), digits: 2, value: savedTextScale ?? 1.0, valign: Gtk.Align.CENTER, }); const textScaleRow = new Adw.ActionRow({ title: 'Text scale', subtitle: savedTextScale === null ? 'Not captured by this older profile. Set a value to apply it with the layout.' : 'Applied with this display profile.', }); textScaleRow.add_suffix(textScale); row.add_row(textScaleRow); textScale.connect('value-changed', widget => { setTextScalingFactor(this._profiles[profileIndex], widget.get_value()); textScaleRow.subtitle = 'Applied with this display profile.'; this._saveProfiles(); }); } _saveProfiles() { this._settings.set_value('profiles', new GLib.Variant(PROFILE_VARIANT_TYPE, this._profiles)); } }