Strata/strata@edu4rdshl.dev/prefs.js
Eduard Tolosa 43a68e7970 extension: cleanup before any future EGO submission
Remove narration breadcrumb comments, the unused _strataPreview field, the
self-referencing this.actor alias, and three non-error console.log calls.
Rename #busy to _busy for consistency with the rest of the class. Tighten a
couple of over-long docstrings.
2026-06-25 15:33:13 -05:00

346 lines
13 KiB
JavaScript

/* prefs.js - Strata preferences window. */
import Adw from 'gi://Adw';
import Gtk from 'gi://Gtk';
import Gdk from 'gi://Gdk';
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
export default class StrataPreferences extends ExtensionPreferences {
fillPreferencesWindow(window) {
const settings = this.getSettings();
window.add(this._buildGeneralPage(settings));
window.add(this._buildPrivacyPage(settings));
}
_buildGeneralPage(settings) {
const page = new Adw.PreferencesPage({
title: 'General',
icon_name: 'preferences-system-symbolic',
});
const historyGroup = new Adw.PreferencesGroup({ title: 'History' });
const maxHistoryRow = new Adw.SpinRow({
title: 'Maximum items',
subtitle: 'All items are searchable and reachable; older are deleted past this limit',
adjustment: new Gtk.Adjustment({
lower: 50,
upper: 2000,
step_increment: 10,
page_increment: 50,
}),
});
settings.bind('max-history', maxHistoryRow, 'value', Gio.SettingsBindFlags.DEFAULT);
historyGroup.add(maxHistoryRow);
const pageSizeRow = new Adw.SpinRow({
title: 'Items per page',
subtitle: 'Rows fetched on open and on each scroll-to-bottom',
adjustment: new Gtk.Adjustment({
lower: 20,
upper: 200,
step_increment: 10,
page_increment: 50,
}),
});
settings.bind('page-size', pageSizeRow, 'value', Gio.SettingsBindFlags.DEFAULT);
historyGroup.add(pageSizeRow);
const maxTextRow = new Adw.SpinRow({
title: 'Maximum text size (MB)',
subtitle: 'Text payloads larger than this are not stored',
adjustment: new Gtk.Adjustment({
lower: 1,
upper: 100,
step_increment: 1,
page_increment: 5,
}),
});
settings.bind('max-text-mb', maxTextRow, 'value', Gio.SettingsBindFlags.DEFAULT);
historyGroup.add(maxTextRow);
const maxImageRow = new Adw.SpinRow({
title: 'Maximum image size (MB)',
subtitle: 'Image payloads larger than this are not stored',
adjustment: new Gtk.Adjustment({
lower: 1,
upper: 100,
step_increment: 1,
page_increment: 5,
}),
});
settings.bind('max-image-mb', maxImageRow, 'value', Gio.SettingsBindFlags.DEFAULT);
historyGroup.add(maxImageRow);
page.add(historyGroup);
const appearanceGroup = new Adw.PreferencesGroup({ title: 'Appearance' });
const themes = [
{ id: 'auto', label: 'Automatic' },
{ id: 'light', label: 'Light' },
{ id: 'dark', label: 'Dark' },
];
const themeRow = new Adw.ComboRow({
title: 'Theme',
subtitle: 'Automatic follows the system light/dark preference',
model: Gtk.StringList.new(themes.map(t => t.label)),
});
const currentTheme = settings.get_string('theme');
const currentThemeIdx = themes.findIndex(t => t.id === currentTheme);
themeRow.selected = currentThemeIdx >= 0 ? currentThemeIdx : 0;
themeRow.connect('notify::selected', () => {
settings.set_string('theme', themes[themeRow.selected].id);
});
settings.connect('changed::theme', () => {
const idx = themes.findIndex(t => t.id === settings.get_string('theme'));
if (idx >= 0 && themeRow.selected !== idx)
themeRow.selected = idx;
});
appearanceGroup.add(themeRow);
const panelWidthRow = new Adw.SpinRow({
title: 'Panel width',
subtitle: 'Width of the clipboard panel in pixels',
adjustment: new Gtk.Adjustment({
lower: 200,
upper: 1200,
step_increment: 10,
page_increment: 50,
}),
});
settings.bind('panel-width', panelWidthRow, 'value', Gio.SettingsBindFlags.DEFAULT);
appearanceGroup.add(panelWidthRow);
const panelMaxHeightRow = new Adw.SpinRow({
title: 'Panel max height',
subtitle: 'Maximum height before the list scrolls',
adjustment: new Gtk.Adjustment({
lower: 200,
upper: 1200,
step_increment: 10,
page_increment: 50,
}),
});
settings.bind('panel-max-height', panelMaxHeightRow, 'value', Gio.SettingsBindFlags.DEFAULT);
appearanceGroup.add(panelMaxHeightRow);
const moveToTopRow = new Adw.SwitchRow({
title: 'Move activated item to top',
subtitle: 'Selecting an item moves it to the top of the list',
});
settings.bind('move-activated-to-top', moveToTopRow, 'active', Gio.SettingsBindFlags.DEFAULT);
appearanceGroup.add(moveToTopRow);
const positions = [
{ id: 'top-center', label: 'Top center' },
{ id: 'top-left', label: 'Top left' },
{ id: 'top-right', label: 'Top right' },
{ id: 'center', label: 'Center' },
{ id: 'bottom-center', label: 'Bottom center' },
{ id: 'bottom-left', label: 'Bottom left' },
{ id: 'bottom-right', label: 'Bottom right' },
];
const positionRow = new Adw.ComboRow({
title: 'Panel position',
subtitle: 'Where the clipboard panel appears on screen',
model: Gtk.StringList.new(positions.map(p => p.label)),
});
const currentPos = settings.get_string('panel-position');
const currentIdx = positions.findIndex(p => p.id === currentPos);
positionRow.selected = currentIdx >= 0 ? currentIdx : 0;
positionRow.connect('notify::selected', () => {
settings.set_string('panel-position', positions[positionRow.selected].id);
});
settings.connect('changed::panel-position', () => {
const idx = positions.findIndex(p => p.id === settings.get_string('panel-position'));
if (idx >= 0 && positionRow.selected !== idx)
positionRow.selected = idx;
});
appearanceGroup.add(positionRow);
page.add(appearanceGroup);
const kbGroup = new Adw.PreferencesGroup({ title: 'Keyboard' });
const shortcutRow = new Adw.ActionRow({
title: 'Open Strata',
subtitle: 'Click to change the keyboard shortcut',
activatable: true,
});
const shortcutLabel = new Gtk.ShortcutLabel({
valign: Gtk.Align.CENTER,
disabled_text: 'Disabled',
});
const updateShortcutLabel = () => {
const shortcuts = settings.get_strv('keyboard-shortcut');
shortcutLabel.accelerator = shortcuts[0] ?? '';
};
updateShortcutLabel();
settings.connect('changed::keyboard-shortcut', updateShortcutLabel);
shortcutRow.add_suffix(shortcutLabel);
shortcutRow.connect('activated', () => {
this._showShortcutDialog(shortcutRow.get_root(), settings);
});
kbGroup.add(shortcutRow);
page.add(kbGroup);
return page;
}
_buildPrivacyPage(settings) {
const page = new Adw.PreferencesPage({
title: 'Privacy',
icon_name: 'security-high-symbolic',
});
const group = new Adw.PreferencesGroup({
title: 'App Exclusions',
description: 'Items copied while these apps have focus will not be stored in history. Enter a partial app name (case-insensitive).',
});
const model = new Gtk.StringList();
const currentApps = settings.get_strv('excluded-apps');
for (const app of currentApps)
model.append(app);
/** Sync the StringList back to GSettings. */
const saveModel = () => {
const apps = [];
for (let i = 0; i < model.get_n_items(); i++) {
const val = model.get_string(i);
if (val?.trim()) apps.push(val.trim());
}
settings.set_strv('excluded-apps', apps);
};
const listBox = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ['boxed-list'],
});
const rebuildList = () => {
let child = listBox.get_first_child();
while (child) {
const next = child.get_next_sibling();
listBox.remove(child);
child = next;
}
for (let i = 0; i < model.get_n_items(); i++) {
const idx = i;
const row = new Adw.ActionRow({ activatable: false });
const label = new Gtk.EditableLabel({
text: model.get_string(i),
valign: Gtk.Align.CENTER,
hexpand: true,
});
label.connect('changed', () => {
model.splice(idx, 1, [label.text]);
saveModel();
});
const removeBtn = new Gtk.Button({
icon_name: 'list-remove-symbolic',
valign: Gtk.Align.CENTER,
css_classes: ['flat', 'destructive-action'],
tooltip_text: 'Remove',
});
removeBtn.connect('clicked', () => {
model.remove(idx);
saveModel();
rebuildList();
});
row.add_suffix(label);
row.add_suffix(removeBtn);
listBox.append(row);
}
const addRow = new Adw.ActionRow({ activatable: false });
const addEntry = new Gtk.Entry({
placeholder_text: 'App name…',
valign: Gtk.Align.CENTER,
hexpand: true,
});
const addBtn = new Gtk.Button({
icon_name: 'list-add-symbolic',
valign: Gtk.Align.CENTER,
css_classes: ['flat'],
tooltip_text: 'Add',
});
const doAdd = () => {
const val = addEntry.text.trim();
if (val) {
model.append(val);
saveModel();
rebuildList();
}
};
addBtn.connect('clicked', doAdd);
addEntry.connect('activate', doAdd);
addRow.add_suffix(addEntry);
addRow.add_suffix(addBtn);
listBox.append(addRow);
};
rebuildList();
group.add(listBox);
page.add(group);
return page;
}
_showShortcutDialog(parent, settings) {
const dialog = new Adw.MessageDialog({
heading: 'Set Keyboard Shortcut',
body: 'Press the desired key combination, or Backspace to clear.',
transient_for: parent,
modal: true,
});
dialog.add_response('cancel', 'Cancel');
dialog.add_response('clear', 'Clear');
const label = new Gtk.ShortcutLabel({
accelerator: settings.get_strv('keyboard-shortcut')[0] ?? '',
disabled_text: '(none)',
margin_top: 12,
margin_bottom: 12,
halign: Gtk.Align.CENTER,
});
dialog.set_extra_child(label);
const controller = new Gtk.EventControllerKey();
controller.connect('key-pressed', (_ctrl, keyval, keycode, state) => {
const mods = state & Gtk.accelerator_get_default_mod_mask();
if (keyval === Gdk.KEY_BackSpace) {
label.accelerator = '';
return true;
}
if (keyval === Gdk.KEY_Escape) {
dialog.close();
return true;
}
if (Gtk.accelerator_valid(keyval, mods)) {
label.accelerator = Gtk.accelerator_name(keyval, mods);
settings.set_strv('keyboard-shortcut',
label.accelerator ? [label.accelerator] : []);
dialog.close();
}
return true;
});
dialog.add_controller(controller);
dialog.connect('response', (_d, response) => {
if (response === 'clear') {
settings.set_strv('keyboard-shortcut', []);
}
dialog.destroy();
});
dialog.present();
}
}