From 185a8e233c31de92ff56c92cef1808d3f037d687 Mon Sep 17 00:00:00 2001 From: Lucas Oskorep Date: Sat, 5 Apr 2025 23:55:46 -0400 Subject: [PATCH] feat: adding in demo settings page for gnome extensions --- Makefile | 36 --- extension.ts | 253 ++++++++++++++---- justfile | 24 +- metadata.json | 8 +- prefs.ts | 4 + ...ome.shell.extensions.aerospike.gschema.xml | 38 ++- src/prefs.ts | 192 +++++++++++++ src/utils.ts | 20 ++ tsconfig.json | 2 + 9 files changed, 482 insertions(+), 95 deletions(-) delete mode 100644 Makefile create mode 100644 prefs.ts create mode 100644 src/prefs.ts create mode 100644 src/utils.ts diff --git a/Makefile b/Makefile deleted file mode 100644 index ee74702..0000000 --- a/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -NAME=aerospike -DOMAIN=lucaso.io - -.PHONY: all pack install clean - -all: dist/extension.js - -node_modules: package.json - pnpm install - -dist/extension.js : node_modules - tsc - -schemas/gschemas.compiled: schemas/org.gnome.shell.extensions.$(NAME).gschema.xml - glib-compile-schemas schemas - -$(NAME).zip: dist/extension.js dist/prefs.js schemas/gschemas.compiled - @rm -rf dist/* - @cp metadata.json dist/ - @cp stylesheet.css dist/ - @mkdir dist/schemas - @cp schemas/*.compiled dist/schemas/ - @(cd dist && zip ../$(NAME).zip -9r .) - -pack: $(NAME).zip - -install: $(NAME).zip - -clean: - @rm -rf dist node_modules $(NAME).zip - -test: - @dbus-run-session -- gnome-shell --nested --wayland - -.PHONY: install-and-test -install-and-test: install test diff --git a/extension.ts b/extension.ts index 4bab2a7..9693118 100644 --- a/extension.ts +++ b/extension.ts @@ -2,8 +2,11 @@ import GLib from 'gi://GLib'; import St from 'gi://St'; import Meta from 'gi://Meta'; import {Extension, ExtensionMetadata} from 'resource:///org/gnome/shell/extensions/extension.js'; +import Mtk from "@girs/mtk-16"; -// import Gio from 'gi://Gio'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import Gio from 'gi://Gio'; +import Shell from 'gi://Shell'; // import cairo from "cairo"; // import Shell from 'gi://Shell'; // import * as Main from 'resource:///org/gnome/shell/ui/main.js'; @@ -18,8 +21,11 @@ type Signal = { id: number; } -export default class aerospike extends Extension { + +export default class aerospike extends Extension { + settings: Gio.Settings; + keyBindings: Map; borderActor: St.Widget | null; focusWindowSignals: any[]; lastFocusedWindow: Meta.Window | null; @@ -30,6 +36,8 @@ export default class aerospike extends Extension { constructor(metadata: ExtensionMetadata) { super(metadata); + this.settings = this.getSettings('org.gnome.shell.extensions.aerospike'); + this.keyBindings = new Map(); // Initialize instance variables this.borderActor = null; this.focusWindowSignals = []; @@ -43,8 +51,7 @@ export default class aerospike extends Extension { enable() { console.log("STARTING AEROSPIKE!") - - // this._captureExistingWindows(); + this._captureExistingWindows(); // Connect window signals this._windowCreateId = global.display.connect( 'window-created', @@ -52,6 +59,111 @@ export default class aerospike extends Extension { this.handleWindowCreated(window); } ); + this.bindSettings(); + } + + + private bindSettings() { + // Monitor settings changes + this.settings.connect('changed::keybinding-1', () => { + log(`Keybinding 1 changed to: ${this.settings.get_strv('keybinding-1')}`); + this.refreshKeybinding('keybinding-1'); + }); + + this.settings.connect('changed::keybinding-2', () => { + log(`Keybinding 2 changed to: ${this.settings.get_strv('keybinding-2')}`); + this.refreshKeybinding('keybinding-2'); + }); + + this.settings.connect('changed::keybinding-3', () => { + log(`Keybinding 3 changed to: ${this.settings.get_strv('keybinding-3')}`); + this.refreshKeybinding('keybinding-3'); + }); + + this.settings.connect('changed::keybinding-4', () => { + log(`Keybinding 4 changed to: ${this.settings.get_strv('keybinding-4')}`); + this.refreshKeybinding('keybinding-4'); + }); + + this.settings.connect('changed::dropdown-option', () => { + log(`Dropdown option changed to: ${this.settings.get_string('dropdown-option')}`); + }); + + this.settings.connect('changed::color-selection', () => { + log(`Color selection changed to: ${this.settings.get_string('color-selection')}`); + }); + } + private refreshKeybinding(settingName: string) { + if (this.keyBindings.has(settingName)) { + Main.wm.removeKeybinding(settingName); + this.keyBindings.delete(settingName); + } + + switch (settingName) { + case 'keybinding-1': + this.bindKeybinding('keybinding-1', () => { + log('Keybinding 1 was pressed!'); + }); + break; + case 'keybinding-2': + this.bindKeybinding('keybinding-2', () => { + log('Keybinding 2 was pressed!'); + }); + break; + case 'keybinding-3': + this.bindKeybinding('keybinding-3', () => { + log('Keybinding 3 was pressed!'); + }); + break; + case 'keybinding-4': + this.bindKeybinding('keybinding-4', () => { + log('Keybinding 4 was pressed!'); + }); + break; + } + } + + private removeKeybindings() { + this.keyBindings.forEach((_, key) => { + Main.wm.removeKeybinding(key); + }); + this.keyBindings.clear(); + } + + private setupKeybindings() { + this.bindKeybinding('keybinding-1', () => { + log('Keybinding 1 was pressed!'); + }); + + this.bindKeybinding('keybinding-2', () => { + log('Keybinding 2 was pressed!'); + }); + + this.bindKeybinding('keybinding-3', () => { + log('Keybinding 3 was pressed!'); + }); + + this.bindKeybinding('keybinding-4', () => { + log('Keybinding 4 was pressed!'); + }); + } + + private bindKeybinding(settingName: string, callback: () => void) { + const keyBindingSettings = this.settings.get_strv(settingName); + + if (keyBindingSettings.length === 0 || keyBindingSettings[0] === '') { + return; + } + + const keyBindingAction = Main.wm.addKeybinding( + settingName, + this.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL, + callback + ); + + this.keyBindings.set(settingName, keyBindingAction); } handleWindowCreated(window: Meta.Window) { @@ -69,19 +181,19 @@ export default class aerospike extends Extension { this._addWindow(window); } - // _captureExistingWindows() { - // console.log("CAPTURING WINDOWS") - // const workspace = global.workspace_manager.get_active_workspace(); - // const windows = global.display.get_tab_list(Meta.TabList.NORMAL, workspace); - // console.log("WINDOWS", windows); - // windows.forEach(window => { - // if (this._isWindowTileable(window)) { - // this._addWindow(window); - // } - // }); - // - // // this._tileWindows(); - // } + _captureExistingWindows() { + console.log("CAPTURING WINDOWS") + const workspace = global.workspace_manager.get_active_workspace(); + const windows = global.display.get_tab_list(Meta.TabList.NORMAL, workspace); + console.log("WINDOWS", windows); + windows.forEach(window => { + if (this._isWindowTileable(window)) { + this._addWindow(window); + } + }); + + this._tileWindows(); + } getUsableMonitorSpace(window: Meta.Window) { // Get the current workspace @@ -101,21 +213,70 @@ export default class aerospike extends Extension { }; } + + // Function to safely resize a window after it's ready + safelyResizeWindow(win: Meta.Window, x: number, y: number, width: number, height: number): void { + const actor = win.get_compositor_private(); + + if (!actor) { + console.log("No actor available, can't resize safely yet"); + return; + } + + // Set a flag to track if the resize has been done + let resizeDone = false; + + // Connect to the first-frame signal + const id = actor.connect('first-frame', () => { + // Disconnect the signal handler + actor.disconnect(id); + + if (!resizeDone) { + resizeDone = true; + + // Add a small delay + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { + try { + this.resizeWindow(win, x, y, width, height); + } catch (e) { + console.error("Error resizing window:", e); + } + return GLib.SOURCE_REMOVE; + }); + } + }); + + // Fallback timeout in case the first-frame signal doesn't fire + // (for windows that are already mapped) + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + if (!resizeDone) { + resizeDone = true; + try { + this.resizeWindow(win, x, y, width, height); + } catch (e) { + console.error("Error resizing window (fallback):", e); + } + } + return GLib.SOURCE_REMOVE; + }); + } + resizeWindow(win: Meta.Window, x:number, y:number, width:number, height:number) { // First, ensure window is not maximized or fullscreen - // if (win.get_maximized()) { - // console.log("WINDOW MAXIMIZED") - // win.unmaximize(Meta.MaximizeFlags.BOTH); - // } - // - // if (win.is_fullscreen()) { - // console.log("WINDOW IS FULLSCREEN") - // win.unmake_fullscreen(); - // } + if (win.get_maximized()) { + console.log("WINDOW MAXIMIZED") + win.unmaximize(Meta.MaximizeFlags.BOTH); + } + + if (win.is_fullscreen()) { + console.log("WINDOW IS FULLSCREEN") + win.unmake_fullscreen(); + } console.log("WINDOW", win.get_window_type(), win.allows_move()); console.log("MONITOR INFO", this.getUsableMonitorSpace(win)); console.log("NEW_SIZE", x, y, width, height); - win.move_resize_frame(false, 50, 50, 300, 300); + // win.move_resize_frame(false, 50, 50, 300, 300); + win.move_resize_frame(false, x, y, width, height); console.log("RESIZED WINDOW", win.get_frame_rect().height, win.get_frame_rect().width, win.get_frame_rect().x, win.get_frame_rect().y); } @@ -131,18 +292,18 @@ export default class aerospike extends Extension { // act.disconnect(id); // }); - // const destroyId = window.connect('unmanaging', () => { - // console.log("REMOVING WINDOW", windowId); - // this._handleWindowClosed(windowId); - // }); - // signals.push({name: 'unmanaging', id: destroyId}); + const destroyId = window.connect('unmanaging', () => { + console.log("REMOVING WINDOW", windowId); + this._handleWindowClosed(windowId); + }); + signals.push({name: 'unmanaging', id: destroyId}); - // const focusId = window.connect('notify::has-focus', () => { - // if (window.has_focus()) { - // this._activeWindowId = windowId; - // } - // }); - // signals.push({name: 'notify::has-focus', id: focusId}); + const focusId = window.connect('notify::has-focus', () => { + if (window.has_focus()) { + this._activeWindowId = windowId; + } + }); + signals.push({name: 'notify::has-focus', id: focusId}); // Add window to managed windows this._windows.set(windowId, { @@ -159,7 +320,7 @@ export default class aerospike extends Extension { } _handleWindowClosed(windowId: number) { - + print("closing window", windowId); const windowData = this._windows.get(windowId); if (!windowData) { return; @@ -205,12 +366,12 @@ export default class aerospike extends Extension { // Get all windows for current workspace const windows = Array.from(this._windows.values()) - // .filter(({window}) => { - // - // if (window != null) { - // return window.get_workspace() === workspace; - // } - // }) + .filter(({window}) => { + + if (window != null) { + return window.get_workspace() === workspace; + } + }) .map(({window}) => window); if (windows.length === 0) { @@ -232,7 +393,7 @@ export default class aerospike extends Extension { height: workArea.height }; if (window != null) { - this.resizeWindow(window, rect.x, rect.y, rect.width, rect.height); + this.safelyResizeWindow(window, rect.x, rect.y, rect.width, rect.height); } }); } diff --git a/justfile b/justfile index acc5aff..bea9a43 100644 --- a/justfile +++ b/justfile @@ -1,19 +1,22 @@ set dotenv-load NAME:="aerospike" DOMAIN:="lucaso.io" +FULL_NAME:=NAME + "@" + DOMAIN packages: pnpm install -build: packages +build: packages && build-schemas rm -rf dist/* tsc - glib-compile-schemas schemas cp metadata.json dist/ cp stylesheet.css dist/ - mkdir dist/schemas - cp schemas/*.compiled dist/schemas/ + mkdir -p dist/schemas +build-schemas: + glib-compile-schemas schemas + cp schemas/org.gnome.shell.extensions.aerospike.gschema.xml dist/schemas/ + cp schemas/gschemas.compiled dist/schemas/ build-package: build cd dist && zip ../{{NAME}}.zip -9r . @@ -25,6 +28,15 @@ install: build cp -r dist/* ~/.local/share/gnome-shell/extensions/{{NAME}}@{{DOMAIN}}/ run: - dbus-run-session -- gnome-shell --nested --wayland + env MUTTER_DEBUG_DUMMY_MODE_SPECS=1280x720 dbus-run-session -- gnome-shell --nested --wayland -install-and-run: install run \ No newline at end of file +install-and-run: install run + +#pack: build +# gnome-extensions pack dist \ +# --force \ +# --out-dir . \ +# --schema ../schemas/org.gnome.shell.extensions.aerospike.gschema.xml +# +#install-pack: pack +# gnome-extensions install ./{{FULL_NAME}}.shell-extension.zip --force \ No newline at end of file diff --git a/metadata.json b/metadata.json index b5250bd..cb3fd81 100644 --- a/metadata.json +++ b/metadata.json @@ -1,9 +1,11 @@ { "name": "aerospike", - "description": "Adds pretty rainbow or static borders to the active and inactive windows", + "description": "I3 Like Tiling Window Manager for Gnome", "uuid": "aerospike@lucaso.io", + "settings-schema": "org.gnome.shell.extensions.aerospike", "shell-version": [ - "47", "48" - ] + ], + "gettext-domain": "aerospike@lucaso.io", + "url": "https://gitea.chaosdev.gay/lucasoskorep/aerospike@lucaso.io" } diff --git a/prefs.ts b/prefs.ts new file mode 100644 index 0000000..b809720 --- /dev/null +++ b/prefs.ts @@ -0,0 +1,4 @@ +// This file is just a wrapper around the compiled TypeScript code +import MyExtensionPreferences from './src/prefs.js'; + +export default MyExtensionPreferences; \ No newline at end of file diff --git a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml index f3bd209..f137b01 100644 --- a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml +++ b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml @@ -1,10 +1,40 @@ - - "Horizontal" - Type of tiling - The type of tiling provided by aerospace + + 1']]]> + Keybinding for action 1 + Keyboard shortcut for triggering action 1 + + + + 2']]]> + Keybinding for action 2 + Keyboard shortcut for triggering action 2 + + + + 3']]]> + Keybinding for action 3 + Keyboard shortcut for triggering action 3 + + + + 4']]]> + Keybinding for action 4 + Keyboard shortcut for triggering action 4 + + + + 'option1' + Dropdown selection + Option selected from the dropdown menu + + + + 'rgb(255,0,0)' + Selected color + Color chosen from the color picker \ No newline at end of file diff --git a/src/prefs.ts b/src/prefs.ts new file mode 100644 index 0000000..57dbcf0 --- /dev/null +++ b/src/prefs.ts @@ -0,0 +1,192 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import Gtk from 'gi://Gtk'; +import Gdk from 'gi://Gdk'; +import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +export default class MyExtensionPreferences extends ExtensionPreferences { + async fillPreferencesWindow(window: Adw.PreferencesWindow) { + // Create settings object + const settings = this.getSettings('org.gnome.shell.extensions.aerospike'); + + // Create a preferences page + const page = new Adw.PreferencesPage({ + title: _('Settings'), + icon_name: 'preferences-system-symbolic', + }); + window.add(page); + + // Create keybindings group + const keybindingsGroup = new Adw.PreferencesGroup({ + title: _('Keyboard Shortcuts'), + }); + page.add(keybindingsGroup); + + // Add keybinding rows + this.addKeybindingRow(keybindingsGroup, settings, 'keybinding-1', _('Action 1')); + this.addKeybindingRow(keybindingsGroup, settings, 'keybinding-2', _('Action 2')); + this.addKeybindingRow(keybindingsGroup, settings, 'keybinding-3', _('Action 3')); + this.addKeybindingRow(keybindingsGroup, settings, 'keybinding-4', _('Action 4')); + + // Create options group + const optionsGroup = new Adw.PreferencesGroup({ + title: _('Options'), + }); + page.add(optionsGroup); + + // Add dropdown + const dropdownRow = new Adw.ComboRow({ + title: _('Select an option'), + }); + optionsGroup.add(dropdownRow); + + // Create dropdown model + const dropdownModel = new Gtk.StringList(); + dropdownModel.append(_('Option 1')); + dropdownModel.append(_('Option 2')); + dropdownModel.append(_('Option 3')); + dropdownModel.append(_('Option 4')); + + dropdownRow.set_model(dropdownModel); + + // Set the active option based on settings + const currentOption = settings.get_string('dropdown-option'); + switch (currentOption) { + case 'option1': + dropdownRow.set_selected(0); + break; + case 'option2': + dropdownRow.set_selected(1); + break; + case 'option3': + dropdownRow.set_selected(2); + break; + case 'option4': + dropdownRow.set_selected(3); + break; + default: + dropdownRow.set_selected(0); + } + + // Connect dropdown change signal + dropdownRow.connect('notify::selected', () => { + const selected = dropdownRow.get_selected(); + let optionValue: string; + + switch (selected) { + case 0: + optionValue = 'option1'; + break; + case 1: + optionValue = 'option2'; + break; + case 2: + optionValue = 'option3'; + break; + case 3: + optionValue = 'option4'; + break; + default: + optionValue = 'option1'; + } + + settings.set_string('dropdown-option', optionValue); + }); + + // Add color button + const colorRow = new Adw.ActionRow({ + title: _('Choose a color'), + }); + optionsGroup.add(colorRow); + + const colorButton = new Gtk.ColorButton(); + colorRow.add_suffix(colorButton); + colorRow.set_activatable_widget(colorButton); + + // Set current color from settings + const colorStr = settings.get_string('color-selection'); + const rgba = new Gdk.RGBA(); + rgba.parse(colorStr); + colorButton.set_rgba(rgba); + + // Connect color button signal + colorButton.connect('color-set', () => { + const color = colorButton.get_rgba().to_string(); + settings.set_string('color-selection', color); + }); + } + + private addKeybindingRow( + group: Adw.PreferencesGroup, + settings: Gio.Settings, + key: string, + title: string + ) { + const shortcutsRow = new Adw.ActionRow({ + title: title, + }); + + group.add(shortcutsRow); + + // Create a button for setting shortcuts + const shortcutButton = new Gtk.Button({ + valign: Gtk.Align.CENTER, + label: settings.get_strv(key)[0] || _("Disabled") + }); + + shortcutsRow.add_suffix(shortcutButton); + shortcutsRow.set_activatable_widget(shortcutButton); + + // When clicking the button, show a dialog or start listening for keystroke + shortcutButton.connect('clicked', () => { + // Show a simple popup stating that the shortcut is being recorded + const dialog = new Gtk.MessageDialog({ + modal: true, + text: _("Press a key combination to set as shortcut"), + secondary_text: _("Press Esc to cancel or Backspace to disable"), + buttons: Gtk.ButtonsType.CANCEL, + transient_for: group.get_root() as Gtk.Window + }); + + // Create a keypress event controller + const controller = new Gtk.EventControllerKey(); + dialog.add_controller(controller); + + controller.connect('key-pressed', (_controller, keyval, keycode, state) => { + // Get the key name + let keyName = Gdk.keyval_name(keyval); + + // Handle special cases + if (keyName === 'Escape') { + dialog.response(Gtk.ResponseType.CANCEL); + return Gdk.EVENT_STOP; + } else if (keyName === 'BackSpace') { + // Clear the shortcut + settings.set_strv(key, []); + shortcutButton.set_label(_("Disabled")); + dialog.response(Gtk.ResponseType.OK); + return Gdk.EVENT_STOP; + } + + // Convert modifier state to keybinding modifiers + let modifiers = state & Gtk.accelerator_get_default_mod_mask(); + + // Ignore standalone modifier keys + if (Gdk.ModifierType.SHIFT_MASK <= keyval && keyval <= Gdk.ModifierType.META_MASK) + return Gdk.EVENT_STOP; + + // Create accelerator string + let accelerator = Gtk.accelerator_name(keyval, modifiers); + if (accelerator) { + settings.set_strv(key, [accelerator]); + shortcutButton.set_label(accelerator); + dialog.response(Gtk.ResponseType.OK); + } + + return Gdk.EVENT_STOP; + }); + + dialog.present(); + }); + } +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e9e97d9 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,20 @@ +// Utility functions and type definitions + +/** + * Interface for the extension settings + */ +export interface ExtensionSettings { + keybinding1: string[]; + keybinding2: string[]; + keybinding3: string[]; + keybinding4: string[]; + dropdownOption: string; + colorSelection: string; +} + +/** + * Log a message with the extension name prefix + */ +export function log(message: string): void { + console.log(`[MyExtension] ${message}`); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8a7ba90..b9a89fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,8 @@ }, "include": [ "ambient.d.ts", + "prefs.ts", + "src/**/*" ], "files": [ "extension.ts",