From d38a24bf072b8407b21042214b4c36fd59149af7 Mon Sep 17 00:00:00 2001 From: Raul Benencia Date: Wed, 12 Aug 2026 13:36:16 -0700 Subject: gnome: display-profile-switcher extension --- .../prefs.js | 157 +++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 .local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/prefs.js (limited to '.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/prefs.js') diff --git a/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/prefs.js b/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/prefs.js new file mode 100644 index 0000000..3f44f55 --- /dev/null +++ b/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/prefs.js @@ -0,0 +1,157 @@ +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)); + } +} -- cgit v1.2.3