aboutsummaryrefslogtreecommitdiff
path: root/.local/share/gnome-shell/extensions/display-profile-switcher@rbenencia.name/capture-dialog.js
diff options
context:
space:
mode:
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.js273
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,
+ });
+ }
+});
nihil fit ex nihilo