mirror of
https://github.com/morgan9e/gnome-cursor-overlay
synced 2026-04-14 00:14:12 +09:00
248 lines
10 KiB
JavaScript
248 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
import Adw from 'gi://Adw';
|
|
import Gdk from 'gi://Gdk';
|
|
import Gio from 'gi://Gio';
|
|
import GObject from 'gi://GObject';
|
|
import Gtk from 'gi://Gtk';
|
|
|
|
import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
|
|
|
function newColorButton(settings, key) {
|
|
const btn = new Gtk.ColorDialogButton({
|
|
dialog: new Gtk.ColorDialog({modal: true, with_alpha: false}),
|
|
valign: Gtk.Align.CENTER,
|
|
});
|
|
const rgba = btn.get_rgba();
|
|
rgba.parse(settings.get_string(key));
|
|
btn.set_rgba(rgba);
|
|
|
|
btn.connect('notify::rgba', widget => {
|
|
const rgb = widget.get_rgba().to_string();
|
|
const hex = '#' + rgb
|
|
.replace(/^rgb\(|\s+|\)$/g, '')
|
|
.split(',')
|
|
.map(s => parseInt(s).toString(16).padStart(2, '0'))
|
|
.join('');
|
|
settings.set_string(key, hex);
|
|
});
|
|
return btn;
|
|
}
|
|
|
|
const MODE_LABELS = ['Circle', 'Cursor', 'Image'];
|
|
const MODE_VALUES = ['circle', 'cursor', 'image'];
|
|
|
|
const OverlayPage = GObject.registerClass(
|
|
class OverlayPage extends Adw.PreferencesPage {
|
|
constructor(extensionObject) {
|
|
super({title: 'Overlay', icon_name: 'input-mouse-symbolic'});
|
|
|
|
const settings = extensionObject.getSettings();
|
|
|
|
// Mode
|
|
const modeGroup = new Adw.PreferencesGroup({title: 'Mode'});
|
|
this.add(modeGroup);
|
|
|
|
const modeRow = new Adw.ComboRow({
|
|
title: 'Overlay Mode',
|
|
subtitle: 'Circle ring, tinted cursor, or custom image',
|
|
model: Gtk.StringList.new(MODE_LABELS),
|
|
});
|
|
modeRow.set_selected(Math.max(0, MODE_VALUES.indexOf(settings.get_string('overlay-mode'))));
|
|
modeRow.connect('notify::selected', w => {
|
|
settings.set_string('overlay-mode', MODE_VALUES[w.selected] || 'circle');
|
|
});
|
|
modeGroup.add(modeRow);
|
|
|
|
// Circle
|
|
const circleGroup = new Adw.PreferencesGroup({title: 'Circle Mode'});
|
|
this.add(circleGroup);
|
|
|
|
const radiusRow = new Adw.SpinRow({
|
|
title: 'Radius',
|
|
adjustment: new Gtk.Adjustment({lower: 4, upper: 128, step_increment: 2}),
|
|
value: settings.get_int('circle-radius'),
|
|
});
|
|
radiusRow.adjustment.connect('value-changed', w => settings.set_int('circle-radius', w.value));
|
|
circleGroup.add(radiusRow);
|
|
|
|
const strokeRow = new Adw.SpinRow({
|
|
title: 'Stroke Width',
|
|
adjustment: new Gtk.Adjustment({lower: 1, upper: 16, step_increment: 1}),
|
|
value: settings.get_int('circle-stroke-width'),
|
|
});
|
|
strokeRow.adjustment.connect('value-changed', w => settings.set_int('circle-stroke-width', w.value));
|
|
circleGroup.add(strokeRow);
|
|
|
|
const circleColorRow = new Adw.ActionRow({title: 'Color'});
|
|
circleColorRow.add_suffix(newColorButton(settings, 'circle-color'));
|
|
circleGroup.add(circleColorRow);
|
|
|
|
const circleOpacityRow = new Adw.SpinRow({
|
|
title: 'Opacity',
|
|
adjustment: new Gtk.Adjustment({lower: 0, upper: 100, step_increment: 5}),
|
|
value: settings.get_int('circle-opacity'),
|
|
});
|
|
circleOpacityRow.adjustment.connect('value-changed', w => settings.set_int('circle-opacity', w.value));
|
|
circleGroup.add(circleOpacityRow);
|
|
|
|
// Cursor
|
|
const cursorGroup = new Adw.PreferencesGroup({title: 'Cursor Mode'});
|
|
this.add(cursorGroup);
|
|
|
|
const cursorSizeRow = new Adw.SpinRow({
|
|
title: 'Size',
|
|
subtitle: 'Xcursor size',
|
|
adjustment: new Gtk.Adjustment({lower: 16, upper: 256, step_increment: 8}),
|
|
value: settings.get_int('cursor-size'),
|
|
});
|
|
cursorSizeRow.adjustment.connect('value-changed', w => settings.set_int('cursor-size', w.value));
|
|
cursorGroup.add(cursorSizeRow);
|
|
|
|
const cursorColorRow = new Adw.ActionRow({title: 'Tint Color'});
|
|
cursorColorRow.add_suffix(newColorButton(settings, 'cursor-color'));
|
|
cursorGroup.add(cursorColorRow);
|
|
|
|
const cursorOpacityRow = new Adw.SpinRow({
|
|
title: 'Opacity',
|
|
adjustment: new Gtk.Adjustment({lower: 0, upper: 100, step_increment: 5}),
|
|
value: settings.get_int('cursor-opacity'),
|
|
});
|
|
cursorOpacityRow.adjustment.connect('value-changed', w => settings.set_int('cursor-opacity', w.value));
|
|
cursorGroup.add(cursorOpacityRow);
|
|
|
|
// Image
|
|
const imageGroup = new Adw.PreferencesGroup({title: 'Image Mode'});
|
|
this.add(imageGroup);
|
|
|
|
const imagePathRow = new Adw.ActionRow({
|
|
title: 'Image File',
|
|
subtitle: settings.get_string('image-path') || 'No file selected',
|
|
});
|
|
const browseBtn = new Gtk.Button({label: 'Browse', valign: Gtk.Align.CENTER});
|
|
browseBtn.connect('clicked', () => {
|
|
const dialog = new Gtk.FileDialog({title: 'Select Overlay Image', modal: true});
|
|
const filter = new Gtk.FileFilter();
|
|
filter.set_name('Images');
|
|
for (const t of ['image/png', 'image/bmp', 'image/svg+xml', 'image/jpeg', 'image/gif'])
|
|
filter.add_mime_type(t);
|
|
const store = new Gio.ListStore({item_type: Gtk.FileFilter});
|
|
store.append(filter);
|
|
dialog.set_filters(store);
|
|
|
|
dialog.open(this.get_root(), null, (dlg, result) => {
|
|
try {
|
|
const file = dlg.open_finish(result);
|
|
if (file) {
|
|
const path = file.get_path();
|
|
settings.set_string('image-path', path);
|
|
imagePathRow.set_subtitle(path);
|
|
}
|
|
} catch { /* cancelled */ }
|
|
});
|
|
});
|
|
imagePathRow.add_suffix(browseBtn);
|
|
imageGroup.add(imagePathRow);
|
|
|
|
const imageSizeRow = new Adw.SpinRow({
|
|
title: 'Size',
|
|
adjustment: new Gtk.Adjustment({lower: 8, upper: 512, step_increment: 8}),
|
|
value: settings.get_int('image-size'),
|
|
});
|
|
imageSizeRow.adjustment.connect('value-changed', w => settings.set_int('image-size', w.value));
|
|
imageGroup.add(imageSizeRow);
|
|
|
|
const imageOpacityRow = new Adw.SpinRow({
|
|
title: 'Opacity',
|
|
adjustment: new Gtk.Adjustment({lower: 0, upper: 100, step_increment: 5}),
|
|
value: settings.get_int('image-opacity'),
|
|
});
|
|
imageOpacityRow.adjustment.connect('value-changed', w => settings.set_int('image-opacity', w.value));
|
|
imageGroup.add(imageOpacityRow);
|
|
|
|
// Per-Monitor
|
|
const monitorGroup = new Adw.PreferencesGroup({title: 'Per-Monitor'});
|
|
this.add(monitorGroup);
|
|
|
|
const disableNewRow = new Adw.SwitchRow({
|
|
title: 'Do not enable on new monitors',
|
|
active: settings.get_boolean('disable-new-monitors'),
|
|
});
|
|
disableNewRow.connect('notify::active', w => {
|
|
settings.set_boolean('disable-new-monitors', w.active);
|
|
metaRow.sensitive = w.active;
|
|
});
|
|
monitorGroup.add(disableNewRow);
|
|
|
|
const metaRow = new Adw.SwitchRow({
|
|
title: 'Enable on Meta monitors',
|
|
subtitle: 'Auto-enable on virtual monitors',
|
|
active: settings.get_boolean('enable-meta-monitors'),
|
|
sensitive: settings.get_boolean('disable-new-monitors'),
|
|
});
|
|
metaRow.connect('notify::active', w => {
|
|
settings.set_boolean('enable-meta-monitors', w.active);
|
|
});
|
|
monitorGroup.add(metaRow);
|
|
|
|
const monitorList = Gdk.Display.get_default().get_monitors();
|
|
const nMonitors = monitorList.get_n_items();
|
|
const enabledMonitors = settings.get_strv('enabled-monitors');
|
|
const disabledMonitors = settings.get_strv('disabled-monitors');
|
|
const disableNew = settings.get_boolean('disable-new-monitors');
|
|
const enableMeta = settings.get_boolean('enable-meta-monitors');
|
|
|
|
for (let i = 0; i < nMonitors; i++) {
|
|
const monitor = monitorList.get_item(i);
|
|
const connector = monitor.get_connector();
|
|
const geom = monitor.get_geometry();
|
|
|
|
let active;
|
|
if (enabledMonitors.includes(connector))
|
|
active = true;
|
|
else if (disabledMonitors.includes(connector))
|
|
active = false;
|
|
else if (!disableNew)
|
|
active = true;
|
|
else
|
|
active = enableMeta;
|
|
|
|
const toggle = new Gtk.Switch({active, valign: Gtk.Align.CENTER});
|
|
|
|
const row = new Adw.ActionRow({
|
|
title: connector || `Monitor ${i + 1}`,
|
|
subtitle: `${geom.width}\u00d7${geom.height}`,
|
|
});
|
|
row.add_suffix(toggle);
|
|
row.set_activatable_widget(toggle);
|
|
|
|
toggle.connect('notify::active', widget => {
|
|
const en = settings.get_strv('enabled-monitors');
|
|
const dis = settings.get_strv('disabled-monitors');
|
|
if (widget.active) {
|
|
settings.set_strv('enabled-monitors',
|
|
en.includes(connector) ? en : [...en, connector]);
|
|
settings.set_strv('disabled-monitors',
|
|
dis.filter(c => c !== connector));
|
|
} else {
|
|
settings.set_strv('disabled-monitors',
|
|
dis.includes(connector) ? dis : [...dis, connector]);
|
|
settings.set_strv('enabled-monitors',
|
|
en.filter(c => c !== connector));
|
|
}
|
|
});
|
|
|
|
monitorGroup.add(row);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
export default class CursorOverlayPreferences extends ExtensionPreferences {
|
|
fillPreferencesWindow(window) {
|
|
window.default_width = 460;
|
|
window.default_height = 880;
|
|
window.add(new OverlayPage(this));
|
|
}
|
|
}
|