diff options
| author | Raul Benencia <raul@thousandeyes.com> | 2026-08-12 13:36:16 -0700 |
|---|---|---|
| committer | Raul Benencia <raul@thousandeyes.com> | 2026-08-12 13:36:16 -0700 |
| commit | d38a24bf072b8407b21042214b4c36fd59149af7 (patch) | |
| tree | 74a197ce036a04397faa3b0ebba6b93ba3e17936 /.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js | |
| parent | bba6b39dfcd7b76807c58afeab85c518dd19d67c (diff) | |
gnome: display-profile-switcher extension
Diffstat (limited to '.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js')
| -rw-r--r-- | .local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js b/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js new file mode 100644 index 0000000..b0fed1b --- /dev/null +++ b/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js @@ -0,0 +1,273 @@ +import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; +import Pango from 'gi://Pango'; +import St from 'gi://St'; + +import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +import {SAVED_CONNECTOR_PROPERTY, escapeRegex} from './display-config.js'; + +export const CaptureDialog = GObject.registerClass( +class CaptureDialog extends ModalDialog.ModalDialog { + _init(profileNames, monitors, scaleGroups, logicalDisplayCount, textScalingFactor, onResponse) { + super._init({destroyOnClose: false}); + this._onResponse = onResponse; + + const box = new St.BoxLayout({ + vertical: true, + style_class: 'prompt-dialog-main-layout', + style: 'width: 38em; spacing: 18px;', + }); + box.add_child(this._wrappedLabel( + 'Save Display Profile', 'headline')); + box.add_child(this._wrappedLabel( + 'Choose how connected displays are matched, then save a new profile or replace an existing one.', + 'description')); + + const matchingSection = this._section('Display Matching'); + matchingSection.add_child(this._wrappedLabel( + 'Enabled fields must match. Each value is an editable regular expression.', + 'description')); + this._monitorFields = []; + this._monitorConnectors = []; + monitors.forEach((monitor, index) => this._addMonitorFields(matchingSection, monitor, index)); + box.add_child(matchingSection); + + const settingsSection = this._section('Display Settings'); + settingsSection.add_child(this._wrappedLabel( + scaleGroups.length === 1 && scaleGroups[0].logicalMonitorIndexes.length > 1 + ? 'This session requires one display scale for all connected displays.' + : 'Display scale controls the size of everything on an individual display.', + 'description')); + this._logicalDisplayCount = logicalDisplayCount; + this._scaleStates = []; + const scaleRows = new St.BoxLayout({ + vertical: true, + style_class: 'popup-menu-content', + style: 'spacing: 2px;', + }); + scaleGroups.forEach(display => this._addDisplayScale(scaleRows, display)); + settingsSection.add_child(scaleRows); + box.add_child(settingsSection); + + const profileSection = this._section('Profile'); + const profileColumns = new St.BoxLayout({x_expand: true, style: 'spacing: 18px;'}); + const newProfileColumn = new St.BoxLayout({vertical: true, x_expand: true, style: 'spacing: 8px;'}); + newProfileColumn.add_child(this._wrappedLabel('New Profile', 'headline')); + this._nameEntry = new St.Entry({ + text: `Profile ${profileNames.length + 1}`, + hint_text: 'Profile Name', + can_focus: true, + x_expand: true, + }); + this._newProfileName = this._nameEntry.get_text(); + this._nameEntry.clutter_text.connect('text-changed', () => { + if (this._selectedProfileName === null) + this._newProfileName = this._nameEntry.get_text(); + }); + this._nameEntry.clutter_text.connect('activate', () => this._respond()); + newProfileColumn.add_child(this._nameEntry); + newProfileColumn.add_child(this._wrappedLabel('Interface Text Scale', 'description')); + this._textScaleEntry = new St.Entry({ + text: textScalingFactor.toFixed(2), + hint_text: '1.00', + can_focus: true, + x_expand: true, + }); + newProfileColumn.add_child(this._textScaleEntry); + profileColumns.add_child(newProfileColumn); + + const overwriteColumn = new St.BoxLayout({vertical: true, x_expand: true, style: 'spacing: 8px;'}); + overwriteColumn.add_child(this._wrappedLabel('Or Overwrite Existing Profile', 'headline')); + this._selectedProfileName = null; + this._profileItems = new Map(); + const profileChoices = new St.BoxLayout({ + vertical: true, + style_class: 'popup-menu-content', + style: 'spacing: 2px;', + }); + if (profileNames.length) { + this._addProfileChoice(profileChoices, null, 'Do Not Overwrite'); + profileNames.forEach(profileName => + this._addProfileChoice(profileChoices, profileName, profileName)); + } else { + profileChoices.add_child(new PopupMenu.PopupMenuItem( + 'No Profiles Available', {reactive: false})); + } + overwriteColumn.add_child(profileChoices); + profileColumns.add_child(overwriteColumn); + profileSection.add_child(profileColumns); + box.add_child(profileSection); + this.contentLayout.add_child(box); + + this.setButtons([ + { + label: 'Cancel', + action: () => this.close(), + key: Clutter.KEY_Escape, + }, + { + label: 'Save', + default: true, + action: () => this._respond(), + }, + ]); + + this.connect('opened', () => this._nameEntry.grab_key_focus()); + } + + _wrappedLabel(text, styleClass) { + const label = new St.Label({text, style_class: styleClass, x_expand: true}); + label.clutter_text.set_line_wrap(true); + label.clutter_text.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR); + label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + return label; + } + + _section(title) { + const section = new St.BoxLayout({vertical: true, style: 'spacing: 8px;'}); + section.add_child(this._wrappedLabel(title, 'headline')); + return section; + } + + _addMonitorFields(box, monitor, index) { + const fields = new Map(); + this._monitorFields.push(fields); + this._monitorConnectors.push(monitor.connector); + box.add_child(this._wrappedLabel( + `Display ${index + 1} — ${monitor.vendor} ${monitor.product} (${monitor.connector})`, + 'headline')); + const rows = new St.BoxLayout({ + vertical: true, + style_class: 'popup-menu-content', + style: 'spacing: 2px;', + }); + this._addField(rows, fields, 'connector', 'Connector / Port', monitor.connector, true); + this._addField(rows, fields, 'vendor', 'Vendor', monitor.vendor, true); + this._addField(rows, fields, 'product', 'Model', monitor.product, true); + this._addField(rows, fields, 'serial', 'Serial Number', monitor.serial, false); + box.add_child(rows); + } + + _addField(box, fields, field, label, value, enabled) { + const row = new St.BoxLayout({x_expand: true, style: 'spacing: 12px;'}); + const item = new PopupMenu.PopupSwitchMenuItem(label, enabled); + item.x_expand = false; + item.style = 'width: 12em;'; + const entry = new St.Entry({ + text: escapeRegex(value), + can_focus: true, + x_expand: true, + hint_text: 'Regular expression', + }); + const setEnabled = isEnabled => { + fields.get(field).enabled = isEnabled; + entry.reactive = isEnabled; + entry.can_focus = isEnabled; + if (isEnabled) + entry.remove_style_pseudo_class('insensitive'); + else + entry.add_style_pseudo_class('insensitive'); + }; + item.connect('toggled', (_item, isEnabled) => setEnabled(isEnabled)); + fields.set(field, {enabled, entry}); + setEnabled(enabled); + row.add_child(item); + row.add_child(entry); + box.add_child(row); + } + + _addDisplayScale(box, display) { + const row = new St.BoxLayout({ + x_expand: true, + style: 'spacing: 12px;', + }); + const label = new St.Label({ + text: `${display.name}${display.primary ? ' — Primary' : ''}`, + x_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }); + const state = {value: display.scale}; + const choices = new St.BoxLayout({style: 'spacing: 4px;'}); + const buttons = []; + display.supportedScales.forEach(scale => { + const button = new St.Button({ + label: this._formatScale(scale), + style_class: 'button', + toggle_mode: true, + checked: Math.abs(scale - display.scale) < 0.001, + can_focus: true, + }); + button.connect('clicked', () => { + state.value = scale; + for (const candidate of buttons) + candidate.checked = candidate === button; + }); + buttons.push(button); + choices.add_child(button); + }); + state.logicalMonitorIndexes = display.logicalMonitorIndexes; + this._scaleStates.push(state); + row.add_child(label); + row.add_child(choices); + box.add_child(row); + } + + _formatScale(scale) { + return `${Math.round(scale * 100)}%`; + } + + _addProfileChoice(box, profileName, label) { + const item = new PopupMenu.PopupMenuItem(label); + item.connect('activate', () => this._selectProfile(profileName)); + item.setOrnament(profileName === null ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE); + this._profileItems.set(profileName, item); + box.add_child(item); + } + + _selectProfile(profileName) { + const previousProfileName = this._selectedProfileName; + this._selectedProfileName = profileName; + for (const [name, item] of this._profileItems) + item.setOrnament(name === profileName ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE); + + if (profileName === null) { + if (previousProfileName !== null) + this._nameEntry.set_text(this._newProfileName); + this._nameEntry.reactive = true; + this._nameEntry.can_focus = true; + this._nameEntry.remove_style_pseudo_class('insensitive'); + this._nameEntry.grab_key_focus(); + return; + } + + if (previousProfileName === null) + this._newProfileName = this._nameEntry.get_text(); + this._nameEntry.set_text(profileName); + this._nameEntry.reactive = false; + this._nameEntry.can_focus = false; + this._nameEntry.add_style_pseudo_class('insensitive'); + } + + _respond() { + const rules = this._monitorFields.map((fields, index) => ({ + ...Object.fromEntries([...fields].filter(([, state]) => state.enabled) + .map(([field, state]) => [field, state.entry.get_text().trim()])), + [SAVED_CONNECTOR_PROPERTY]: this._monitorConnectors[index], + })); + const displayScales = Array(this._logicalDisplayCount); + for (const state of this._scaleStates) { + for (const index of state.logicalMonitorIndexes) + displayScales[index] = state.value; + } + this.close(); + this._onResponse({ + rules, + name: this._selectedProfileName ?? this._nameEntry.get_text().trim(), + overwrite: this._selectedProfileName !== null, + textScalingFactor: Number(this._textScaleEntry.get_text()), + displayScales, + }); + } +}); |
