mirror of
https://github.com/edu4rdshl/Strata.git
synced 2026-07-18 07:34:45 +00:00
Add a Theme preference (Automatic / Light / Dark). Automatic follows the
system org.gnome.desktop.interface color-scheme; the dark theme is
unchanged. Light styling lives in light.css, every rule scoped under a
.strata-theme-light class the panel toggles on its root box, loaded into the
St theme context by the extension. St's CSS engine has no custom properties,
so this specificity-based scoped override is how the palette is switched
without generating a stylesheet at runtime; switching is a single class
toggle off the ingest/render hot paths.
Also in this change:
- fix: the keyboard shortcut now hides the panel when it is already open.
The binding lacked Shell.ActionMode.POPUP, so while the panel's modal grab
was active the second press was swallowed and toggle() never ran.
- fix: the search placeholder ("Search...") is now legible in the light
theme; it previously inherited a light-on-dark Shell color.
- pack: bundle light.css via --extra-source (gnome-extensions pack only
auto-includes stylesheet.css), so packaged installs ship the light theme.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
/**
|
|
* prefs.js - Strata preferences window (GNOME 45+ / Adw).
|
|
*
|
|
* Pages:
|
|
* General: max-history SpinRow, keyboard-shortcut ShortcutRow
|
|
* Privacy: excluded-apps ExpanderRow + StringList (one entry per line)
|
|
*/
|
|
|
|
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));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// General page
|
|
// -------------------------------------------------------------------------
|
|
|
|
_buildGeneralPage(settings) {
|
|
const page = new Adw.PreferencesPage({
|
|
title: 'General',
|
|
icon_name: 'preferences-system-symbolic',
|
|
});
|
|
|
|
// ── History ──────────────────────────────────────────────────────────
|
|
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);
|
|
|
|
// ── Appearance ───────────────────────────────────────────────────────
|
|
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);
|
|
|
|
// ── Keyboard ─────────────────────────────────────────────────────────
|
|
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;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Privacy 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).',
|
|
});
|
|
|
|
// We use a StringList model bound to excluded-apps.
|
|
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);
|
|
};
|
|
|
|
// Each item in the list: an EditableLabel + Remove button.
|
|
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);
|
|
}
|
|
|
|
// Add-new 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;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Keyboard shortcut dialog
|
|
// -------------------------------------------------------------------------
|
|
|
|
_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();
|
|
}
|
|
}
|