From 57e28ff77ae8d163ad41ed0c4fc4a953d26f0f48 Mon Sep 17 00:00:00 2001 From: Lucas Oskorep Date: Tue, 20 May 2025 01:20:48 -0400 Subject: [PATCH] feat: enable the window manager to be able to drag across monitors and support keybindings propperly in the extension settings --- .gitignore | 1 + extension.ts | 67 ++++----- prefs.ts | 4 +- ...ome.shell.extensions.aerospike.gschema.xml | 49 +++---- src/prefs/keybindings.ts | 83 +++++++++++ src/prefs/prefs.ts | 131 ++++++++++-------- src/wm/container.ts | 2 +- src/wm/window.ts | 30 ++-- src/wm/windowManager.ts | 110 ++++++++------- 9 files changed, 304 insertions(+), 173 deletions(-) diff --git a/.gitignore b/.gitignore index 9e586f6..3e89041 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules dist /schemas/gschemas.compiled /aerospike.zip +/debug.log diff --git a/extension.ts b/extension.ts index 4af617d..a503b40 100644 --- a/extension.ts +++ b/extension.ts @@ -21,6 +21,7 @@ export default class aerospike extends Extension { enable() { Logger.log("STARTING AEROSPIKE!") this.bindSettings(); + this.setupKeybindings(); this.windowManager.enable() } @@ -32,24 +33,24 @@ export default class aerospike extends Extension { 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::move-left', () => { + log(`Keybinding 1 changed to: ${this.settings.get_strv('move-left')}`); + this.refreshKeybinding('move-left'); }); - 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::move-right', () => { + log(`Keybinding 2 changed to: ${this.settings.get_strv('move-right')}`); + this.refreshKeybinding('move-right'); }); - 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::join-with-left', () => { + log(`Keybinding 3 changed to: ${this.settings.get_strv('join-with-left')}`); + this.refreshKeybinding('join-with-left'); }); - 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::join-with-right', () => { + log(`Keybinding 4 changed to: ${this.settings.get_strv('join-with-right')}`); + this.refreshKeybinding('join-with-right'); }); this.settings.connect('changed::dropdown-option', () => { @@ -67,24 +68,24 @@ export default class aerospike extends Extension { } switch (settingName) { - case 'keybinding-1': - this.bindKeybinding('keybinding-1', () => { - log('Keybinding 1 was pressed!'); + case 'move-left': + this.bindKeybinding('move-left', () => { + Logger.info('Keybinding 1 was pressed!'); }); break; - case 'keybinding-2': - this.bindKeybinding('keybinding-2', () => { - log('Keybinding 2 was pressed!'); + case 'move-right': + this.bindKeybinding('move-right', () => { + Logger.info('Keybinding 2 was pressed!'); }); break; - case 'keybinding-3': - this.bindKeybinding('keybinding-3', () => { - log('Keybinding 3 was pressed!'); + case 'join-with-left': + this.bindKeybinding('join-with-left', () => { + Logger.info('Keybinding 3 was pressed!'); }); break; - case 'keybinding-4': - this.bindKeybinding('keybinding-4', () => { - log('Keybinding 4 was pressed!'); + case 'join-with-right': + this.bindKeybinding('join-with-right', () => { + Logger.info('Keybinding 4 was pressed!'); }); break; } @@ -98,20 +99,20 @@ export default class aerospike extends Extension { } private setupKeybindings() { - this.bindKeybinding('keybinding-1', () => { - log('Keybinding 1 was pressed!'); + this.bindKeybinding('move-left', () => { + Logger.info('Keybinding 1 was pressed!'); }); - this.bindKeybinding('keybinding-2', () => { - log('Keybinding 2 was pressed!'); + this.bindKeybinding('move-right', () => { + Logger.info('Keybinding 2 was pressed!'); }); - this.bindKeybinding('keybinding-3', () => { - log('Keybinding 3 was pressed!'); + this.bindKeybinding('join-with-left', () => { + Logger.info('Keybinding 3 was pressed!'); }); - this.bindKeybinding('keybinding-4', () => { - log('Keybinding 4 was pressed!'); + this.bindKeybinding('join-with-right', () => { + Logger.info('Keybinding 4 was pressed!'); }); } @@ -125,7 +126,7 @@ export default class aerospike extends Extension { const keyBindingAction = Main.wm.addKeybinding( settingName, this.settings, - Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, callback ); diff --git a/prefs.ts b/prefs.ts index 8c1f4cf..bdcc46d 100644 --- a/prefs.ts +++ b/prefs.ts @@ -1,4 +1,4 @@ // This file is just a wrapper around the compiled TypeScript code -import MyExtensionPreferences from './src/prefs/prefs.js'; +import AerospikeExtensions from './src/prefs/prefs.js'; -export default MyExtensionPreferences; \ No newline at end of file +export default AerospikeExtensions; \ 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 f137b01..cdea8da 100644 --- a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml +++ b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml @@ -1,30 +1,6 @@ - - 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 @@ -36,5 +12,30 @@ Selected color Color chosen from the color picker + + + 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 + + \ No newline at end of file diff --git a/src/prefs/keybindings.ts b/src/prefs/keybindings.ts index e69de29..cec41b0 100644 --- a/src/prefs/keybindings.ts +++ b/src/prefs/keybindings.ts @@ -0,0 +1,83 @@ +// Gnome imports +import Adw from 'gi://Adw'; +import Gtk from 'gi://Gtk'; +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; +import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; +import { Logger } from '../utils/logger.js'; + +/** + * EntryRow class for handling text input including keybindings + */ +export class EntryRow extends Adw.EntryRow { + static { + GObject.registerClass(this); + } + + constructor(params: { + title: string, + settings: Gio.Settings, + bind: string, + map?: { + from: (settings: Gio.Settings, bind: string) => string, + to: (settings: Gio.Settings, bind: string, value: string) => void + } + }) { + super({ title: params.title }); + + const { settings, bind, map } = params; + + // When text changes, update settings + this.connect('changed', () => { + const text = this.get_text(); + if (typeof text === 'string') { + if (map) { + map.to(settings, bind, text); + } else { + settings.set_string(bind, text); + } + } + }); + + // Set initial text from settings + const current = map ? map.from(settings, bind) : settings.get_string(bind); + this.set_text(current ?? ''); + + // Add reset button + this.add_suffix( + new ResetButton({ + settings, + bind, + onReset: () => { + this.set_text((map ? map.from(settings, bind) : settings.get_string(bind)) ?? ''); + }, + }) + ); + } +} + +/** + * Reset button for settings + */ +export class ResetButton extends Gtk.Button { + static { + GObject.registerClass(this); + } + + constructor(params: { + settings?: Gio.Settings, + bind: string, + onReset?: () => void + }) { + super({ + icon_name: 'edit-clear-symbolic', + tooltip_text: _('Reset'), + valign: Gtk.Align.CENTER, + }); + + this.connect('clicked', () => { + params.settings?.reset(params.bind); + params.onReset?.(); + }); + } +} \ No newline at end of file diff --git a/src/prefs/prefs.ts b/src/prefs/prefs.ts index b0cebb1..3820fd8 100644 --- a/src/prefs/prefs.ts +++ b/src/prefs/prefs.ts @@ -4,8 +4,9 @@ import Gtk from 'gi://Gtk'; import Gdk from 'gi://Gdk'; import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; import {Logger} from "../utils/logger.js"; +import {EntryRow} from "./keybindings.js"; -export default class MyExtensionPreferences extends ExtensionPreferences { +export default class AerospikeExtensions extends ExtensionPreferences { async fillPreferencesWindow(window: Adw.PreferencesWindow) { // Create settings object const settings = this.getSettings('org.gnome.shell.extensions.aerospike'); @@ -17,17 +18,6 @@ export default class MyExtensionPreferences extends ExtensionPreferences { }); 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({ @@ -115,49 +105,82 @@ export default class MyExtensionPreferences extends ExtensionPreferences { const color = colorButton.get_rgba().to_string(); settings.set_string('color-selection', color); }); + + // Create keybindings group + const keybindingsGroup = new Adw.PreferencesGroup({ + title: _('Keyboard Shortcuts'), + description: `${_("Syntax")}: h, g, h + ${_("Legend")}: - ${_("Windows key")}, - ${_("Control key")} + ${_("Delete text to unset. Press Return key to accept.")}`, + }); + page.add(keybindingsGroup); + + // Add keybinding rows as EntryRows with proper mapping + // Use the helper function to create the map object + const keybindingMap = this.createKeybindingMap(); + + keybindingsGroup.add( + new EntryRow({ + title: _('Action 1'), + settings: settings, + bind: 'move-left', + map: keybindingMap + }) + ); + + keybindingsGroup.add( + new EntryRow({ + title: _('Action 2'), + settings: settings, + bind: 'move-right', + map: keybindingMap + }) + ); + + keybindingsGroup.add( + new EntryRow({ + title: _('Action 3'), + settings: settings, + bind: 'join-with-left', + map: keybindingMap + }) + ); + + keybindingsGroup.add( + new EntryRow({ + title: _('Action 4'), + settings: settings, + bind: 'join-with-right', + map: keybindingMap + }) + ); + + } - 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) => { - - }); - - dialog.present(); - }); + // Helper function to create a keybinding mapping object + private createKeybindingMap() { + return { + from(settings: Gio.Settings, bind: string) { + return settings.get_strv(bind).join(','); + }, + to(settings: Gio.Settings, bind: string, value: string) { + if (!!value) { + const mappings = value.split(',').map((x) => { + const [, key, mods] = Gtk.accelerator_parse(x); + return Gtk.accelerator_valid(key, mods) && Gtk.accelerator_name(key, mods); + }); + // Filter out any false values to ensure we only have strings + const stringMappings = mappings.filter((x): x is string => typeof x === 'string'); + if (stringMappings.length > 0) { + Logger.debug("setting", bind, "to", stringMappings); + settings.set_strv(bind, stringMappings); + } + } else { + // If value deleted, unset the mapping + settings.set_strv(bind, []); + } + }, + }; } } \ No newline at end of file diff --git a/src/wm/container.ts b/src/wm/container.ts index 719c247..159d477 100644 --- a/src/wm/container.ts +++ b/src/wm/container.ts @@ -34,7 +34,7 @@ export default class WindowContainer { // Add window to managed windows this._tiledItems.push(winWrap); this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap); - winWrap.setParent(this); + // winWrap.setParent(this); queueEvent({ name: "tiling-windows", callback: () => { diff --git a/src/wm/window.ts b/src/wm/window.ts index a9daf03..92176a7 100644 --- a/src/wm/window.ts +++ b/src/wm/window.ts @@ -14,6 +14,7 @@ export class WindowWrapper { readonly _windowMinimizedHandler: WindowMinimizedHandler; readonly _signals: number[] = []; _parent: WindowContainer | null = null; + _dragging: boolean = false; constructor( window: Meta.Window, @@ -43,16 +44,23 @@ export class WindowWrapper { return this._window.get_frame_rect(); } - setParent(parent: WindowContainer): void { - this._parent = parent; + startDragging(): void { + this._dragging = true; + } + stopDragging(): void { + this._dragging = false; } - getParent(): WindowContainer | null { - if (this._parent == null) { - Logger.warn(`Attempting to get parent for window without parent ${JSON.stringify(this)}`); - } - return this._parent - } + // setParent(parent: WindowContainer): void { + // this._parent = parent; + // } + // + // getParent(): WindowContainer | null { + // if (this._parent == null) { + // Logger.warn(`Attempting to get parent for window without parent ${JSON.stringify(this)}`); + // } + // return this._parent + // } connectWindowSignals( windowManager: IWindowManager, @@ -114,7 +122,11 @@ export class WindowWrapper { // This is meant to be an exact copy of Forge's move function, renamed to maintain your API safelyResizeWindow(rect: Rect): void { - // Keep minimal logging + // Keep minimal logging + if (this._dragging) { + Logger.info("STOPPED RESIZE BECAUSE ITEM IS BEING DRAGGED") + return + } Logger.log("SAFELY RESIZE", rect.x, rect.y, rect.width, rect.height); const actor = this._window.get_compositor_private(); diff --git a/src/wm/windowManager.ts b/src/wm/windowManager.ts index 573d0ca..0514120 100644 --- a/src/wm/windowManager.ts +++ b/src/wm/windowManager.ts @@ -44,6 +44,7 @@ export default class WindowManager implements IWindowManager { _grabbedWindowMonitor: number = _UNUSED_MONITOR_ID; _grabbedWindowId: number = _UNUSED_WINDOW_ID; + _changingGrabbedMonitor: boolean = false; constructor() { @@ -73,7 +74,6 @@ export default class WindowManager implements IWindowManager { }), global.display.connect("window-entered-monitor", (display, monitor, window) => { Logger.log("WINDOW HAS ENTERED NEW MONITOR!") - // this._moveWindowToMonitor(window, monitor); }), global.display.connect('window-created', (display, window) => { this.handleWindowCreated(display, window); @@ -91,11 +91,11 @@ export default class WindowManager implements IWindowManager { }), ) - this._windowManagerSignals = [ - global.window_manager.connect("show-tile-preview", (_, _metaWindow, _rect, _num) => { - Logger.log("SHOW TITLE PREVIEW!") - }), - ]; + // this._windowManagerSignals = [ + // global.window_manager.connect("show-tile-preview", (_, _metaWindow, _rect, _num) => { + // Logger.log("SHOW TITLE PREVIEW!") + // }), + // ]; this._workspaceManagerSignals = [ global.workspace_manager.connect("showing-desktop-changed", () => { @@ -204,81 +204,75 @@ export default class WindowManager implements IWindowManager { this._grabbedWindowId = _UNUSED_WINDOW_ID; var rect = window.get_frame_rect() Logger.info("Release Location", window.get_monitor(), rect.x, rect.y, rect.width, rect.height) - const old_mon_id = this._grabbedWindowMonitor; - const new_mon_id = window.get_monitor(); - - Logger.info("MONITOR MATCH", old_mon_id !== new_mon_id); - if (old_mon_id !== new_mon_id) { - Logger.trace("MOVING MONITOR"); - let old_mon = this._monitors.get(old_mon_id); - let new_mon = this._monitors.get(new_mon_id); - if (old_mon === undefined || new_mon === undefined) { - return; - } - - let wrapped = old_mon.getWindow(window.get_id()) - if (wrapped === undefined) { - wrapped = new WindowWrapper(window, this.handleWindowMinimized); - } else { - old_mon.removeWindow(wrapped) - } - new_mon.addWindow(wrapped) - } + // previously window was moved to a new monitor here instead of it being fluid during drag events. this._tileMonitors(); Logger.info("monitor_start and monitor_end", this._grabbedWindowMonitor, window.get_monitor()); } _moveWindowToMonitor(window: Meta.Window, monitorId: number): void { + Logger.info("MOVING WINDOW TO MONITOR", window.get_id(), monitorId); let wrapped = undefined; for (const monitor of this._monitors.values()) { wrapped = monitor.getWindow(window.get_id()); if (wrapped !== undefined) { + Logger.error("FOUND WINDOW IN MONITOR") monitor.removeWindow(wrapped); break; } } if (wrapped === undefined) { + Logger.error("WINDOW NOT DEFINED") wrapped = new WindowWrapper(window, this.handleWindowMinimized); wrapped.connectWindowSignals(this); } + + wrapped.startDragging() let new_mon = this._monitors.get(monitorId); new_mon?.addWindow(wrapped) - this._tileMonitors(); + Logger.info("UPDATE MONITOR", new_mon); + this._grabbedWindowMonitor = monitorId; + wrapped.stopDragging(); } public handleWindowPositionChanged(winWrap: WindowWrapper): void { + if (this._changingGrabbedMonitor) { + return; + } if (winWrap.getWindowId() === this._grabbedWindowId) { - const rect = winWrap.getRect(); - // Logger.log("GRABBED WINDOW POSITION CHANGED", rect.x); const [mouseX, mouseY, _] = global.get_pointer(); - this._monitors.get(winWrap.getMonitor())?.itemDragged(winWrap, mouseX, mouseY); - // Log or use the coordinates - // console.log(`Mouse position: X=${mouseX}, Y=${mouseY}`); + let monitorIndex = -1; + for (let i = 0; i < global.display.get_n_monitors(); i++) { + const workArea = global.workspace_manager.get_active_workspace().get_work_area_for_monitor(i); + if (mouseX >= workArea.x && mouseX < workArea.x + workArea.width && + mouseY >= workArea.y && mouseY < workArea.y + workArea.height) { + monitorIndex = i; + break; + } + } + if (monitorIndex === -1) { + return + } + if (monitorIndex !== this._grabbedWindowMonitor) { + this._changingGrabbedMonitor = true; + Logger.log("CHANGING MONITOR FOR WINDOW"); + this._moveWindowToMonitor(winWrap.getWindow(), monitorIndex); + this._changingGrabbedMonitor = false + } + this._monitors.get(monitorIndex)?.itemDragged(winWrap, mouseX, mouseY); } } public handleWindowMinimized(winWrap: WindowWrapper): void { - Logger.warn("WARNING MINIMIZING WINDOW"); - Logger.log("WARNING MINIMIZED", JSON.stringify(winWrap)); const monitor_id = winWrap.getWindow().get_monitor() - Logger.log("WARNING MINIMIZED", monitor_id); - Logger.warn("WARNING MINIMIZED", this._monitors); - this._minimizedItems.set(winWrap.getWindowId(), winWrap); this._monitors.get(monitor_id)?.removeWindow(winWrap); - - Logger.warn("WARNING MINIMIZED ITEMS", JSON.stringify(this._minimizedItems)); this._tileMonitors() } public handleWindowUnminimized(winWrap: WindowWrapper): void { - Logger.log("WINDOW UNMINIMIZED"); - Logger.log("WINDOW UNMINIMIZED", winWrap == null); - // Logger.log("WINDOW UNMINIMIZED", winWrap); - // Logger.log("WINDOW UNMINIMIZED", winWrap.getWindowId()); this._minimizedItems.delete(winWrap.getWindowId()); this._addWindowWrapperToMonitor(winWrap); this._tileMonitors() @@ -292,10 +286,8 @@ export default class WindowManager implements IWindowManager { } public captureExistingWindows() { - Logger.log("CAPTURING WINDOWS") const workspace = global.workspace_manager.get_active_workspace(); const windows = global.display.get_tab_list(Meta.TabList.NORMAL, workspace); - Logger.log("WINDOWS", windows); windows.forEach(window => { if (this._isWindowTileable(window)) { this.addWindowToMonitor(window); @@ -343,9 +335,6 @@ export default class WindowManager implements IWindowManager { } _addWindowWrapperToMonitor(winWrap: WindowWrapper) { - Logger.log("Adding window", JSON.stringify(winWrap)); - Logger.log("Adding window raw", JSON.stringify(winWrap.getWindow())); - Logger.log("Adding window raw", JSON.stringify(winWrap.getWindow().minimized)); if (winWrap.getWindow().minimized) { this._minimizedItems.set(winWrap.getWindow().get_id(), winWrap); } else { @@ -360,14 +349,35 @@ export default class WindowManager implements IWindowManager { } } + block_titles = [ + "org.gnome.Shell.Extensions", + ] + + _isWindowTilingBlocked(window: Meta.Window) : boolean { + Logger.info("title", window.get_title()); + Logger.info("description", window.get_description()); + Logger.info("class", window.get_wm_class()); + Logger.info("class", window.get_wm_class_instance()); + return this.block_titles.some((title) => { + if (window.get_title() === title) { + Logger.log("WINDOW BLOCKED FROM TILING", window.get_title()); + return true; + } + return false; + }); + } + _isWindowTileable(window: Meta.Window) { if (!window || !window.get_compositor_private()) { return false; } - + if (this._isWindowTilingBlocked(window)) { + return false; + } const windowType = window.get_window_type(); - Logger.log("WINDOW TYPE", windowType); + Logger.log("WINDOW TILING CHECK",); + // Skip certain types of windows return !window.is_skip_taskbar() && windowType !== Meta.WindowType.DESKTOP &&