const St = imports.gi.St; const Main = imports.ui.main; const Meta = imports.gi.Meta; const Clutter = imports.gi.Clutter; const Lang = imports.lang; const Mainloop = imports.mainloop; const Settings = imports.ui.settings; let zoneManager; // Default zone layouts const DEFAULT_LAYOUTS = { 'grid-2x2': { name: '2x2 Grid', zones: [ { x: 0, y: 0, width: 0.5, height: 0.5 }, // Top-left { x: 0.5, y: 0, width: 0.5, height: 0.5 }, // Top-right { x: 0, y: 0.5, width: 0.5, height: 0.5 }, // Bottom-left { x: 0.5, y: 0.5, width: 0.5, height: 0.5 } // Bottom-right ] }, 'columns-3': { name: '3 Columns', zones: [ { x: 0, y: 0, width: 0.33, height: 1 }, { x: 0.33, y: 0, width: 0.34, height: 1 }, { x: 0.67, y: 0, width: 0.33, height: 1 } ] }, 'focus-left': { name: 'Focus Left', zones: [ { x: 0, y: 0, width: 0.7, height: 1 }, // Main left { x: 0.7, y: 0, width: 0.3, height: 0.5 }, // Top-right { x: 0.7, y: 0.5, width: 0.3, height: 0.5 } // Bottom-right ] } }; function ZoneManager() { this._init(); } ZoneManager.prototype = { _init: function() { this.overlay = null; this.currentLayout = 'grid-2x2'; this.layouts = DEFAULT_LAYOUTS; this.isShowing = false; this.dragInProgress = false; // Connect to window grab events this._connectSignals(); this._setupKeybindings(); }, _connectSignals: function() { // Monitor for window grab begin/end this.grabOpBeginId = global.display.connect('grab-op-begin', Lang.bind(this, this._onGrabBegin)); this.grabOpEndId = global.display.connect('grab-op-end', Lang.bind(this, this._onGrabEnd)); }, _setupKeybindings: function() { // Add keybinding to show zones overlay (Super+Z) Main.keybindingManager.addHotKey( 'show-zones', 'z', Lang.bind(this, this._toggleZonesOverlay) ); // Add keybindings for quick snap to zones (Super+Numpad) for (let i = 1; i <= 9; i++) { Main.keybindingManager.addHotKey( 'snap-to-zone-' + i, 'KP_' + i, Lang.bind(this, function() { this._snapToZone(i - 1); }) ); } }, _onGrabBegin: function(display, window, op) { // Check if this is a window move operation if (op === Meta.GrabOp.MOVING) { this.dragInProgress = true; // Check if Shift is held - show overlay let mods = global.get_pointer()[2]; if (mods & Clutter.ModifierType.SHIFT_MASK) { this._showZonesOverlay(); } } }, _onGrabEnd: function(display, window, op) { if (op === Meta.GrabOp.MOVING && this.dragInProgress) { this.dragInProgress = false; if (this.isShowing) { // Snap to zone if cursor is over one this._snapWindowToZone(window); this._hideZonesOverlay(); } } }, _toggleZonesOverlay: function() { if (this.isShowing) { this._hideZonesOverlay(); } else { this._showZonesOverlay(); } }, _showZonesOverlay: function() { if (this.isShowing) return; let monitor = Main.layoutManager.primaryMonitor; // Create overlay container this.overlay = new St.Widget({ style_class: 'gridsnap-overlay', reactive: false, x: monitor.x, y: monitor.y, width: monitor.width, height: monitor.height }); // Add semi-transparent background this.overlay.set_style( 'background-color: rgba(0, 0, 0, 0.3);' ); // Draw zones let layout = this.layouts[this.currentLayout]; layout.zones.forEach((zone, index) => { let zoneActor = this._createZoneActor(zone, monitor, index); this.overlay.add_child(zoneActor); }); Main.layoutManager.addChrome(this.overlay); this.isShowing = true; }, _createZoneActor: function(zone, monitor, index) { let x = monitor.width * zone.x; let y = monitor.height * zone.y; let width = monitor.width * zone.width; let height = monitor.height * zone.height; let actor = new St.Widget({ style_class: 'gridsnap-zone', x: x, y: y, width: width, height: height }); actor.set_style( 'border: 2px solid rgba(100, 149, 237, 0.8);' + 'background-color: rgba(100, 149, 237, 0.2);' + 'border-radius: 4px;' ); // Add zone number label let label = new St.Label({ text: String(index + 1), style_class: 'gridsnap-zone-label', x: 10, y: 10 }); label.set_style( 'color: white;' + 'font-size: 24px;' + 'font-weight: bold;' + 'text-shadow: 2px 2px 4px rgba(0,0,0,0.8);' ); actor.add_child(label); return actor; }, _hideZonesOverlay: function() { if (!this.isShowing) return; if (this.overlay) { Main.layoutManager.removeChrome(this.overlay); this.overlay.destroy(); this.overlay = null; } this.isShowing = false; }, _snapWindowToZone: function(window) { if (!window) return; let [x, y] = global.get_pointer(); let monitor = Main.layoutManager.primaryMonitor; // Convert to relative coordinates let relX = (x - monitor.x) / monitor.width; let relY = (y - monitor.y) / monitor.height; // Find which zone the cursor is in let layout = this.layouts[this.currentLayout]; let targetZone = null; for (let zone of layout.zones) { if (relX >= zone.x && relX < zone.x + zone.width && relY >= zone.y && relY < zone.y + zone.height) { targetZone = zone; break; } } if (targetZone) { this._moveWindowToZone(window, targetZone, monitor); } }, _snapToZone: function(zoneIndex) { // Snap the focused window to a specific zone let window = global.display.focus_window; if (!window) return; let layout = this.layouts[this.currentLayout]; if (zoneIndex >= layout.zones.length) return; let monitor = Main.layoutManager.primaryMonitor; this._moveWindowToZone(window, layout.zones[zoneIndex], monitor); }, _moveWindowToZone: function(window, zone, monitor) { // Unmaximize if maximized if (window.get_maximized()) { window.unmaximize(Meta.MaximizeFlags.BOTH); } // Calculate absolute coordinates let x = monitor.x + Math.round(monitor.width * zone.x); let y = monitor.y + Math.round(monitor.height * zone.y); let width = Math.round(monitor.width * zone.width); let height = Math.round(monitor.height * zone.height); // Move and resize window window.move_resize_frame(true, x, y, width, height); }, cycleLayout: function() { let layouts = Object.keys(this.layouts); let currentIndex = layouts.indexOf(this.currentLayout); let nextIndex = (currentIndex + 1) % layouts.length; this.currentLayout = layouts[nextIndex]; Main.notify('GridSnap', 'Layout: ' + this.layouts[this.currentLayout].name); // Refresh overlay if showing if (this.isShowing) { this._hideZonesOverlay(); this._showZonesOverlay(); } }, destroy: function() { // Disconnect signals if (this.grabOpBeginId) { global.display.disconnect(this.grabOpBeginId); } if (this.grabOpEndId) { global.display.disconnect(this.grabOpEndId); } // Remove keybindings Main.keybindingManager.removeHotKey('show-zones'); for (let i = 1; i <= 9; i++) { Main.keybindingManager.removeHotKey('snap-to-zone-' + i); } // Clean up overlay this._hideZonesOverlay(); } }; function init(metadata) { // Extension initialization } function enable() { zoneManager = new ZoneManager(); // Add keybinding to cycle layouts Main.keybindingManager.addHotKey( 'cycle-layout', 'z', Lang.bind(zoneManager, zoneManager.cycleLayout) ); } function disable() { if (zoneManager) { Main.keybindingManager.removeHotKey('cycle-layout'); zoneManager.destroy(); zoneManager = null; } }