Fix critical bugs and improve laptop compatibility

Major fixes:
- Fix keybinding closure bug causing all zones to map to zone 9
- Change keybindings from Super+Numpad to Super+Ctrl+1-9 for laptop compatibility
- Fix shift-drag detection by implementing continuous modifier polling during window drag
- Fix window object reference by using global.display.focus_window
- Fix window API compatibility (use property checks instead of get_maximized())
- Correct grab-op-begin/end signal handler parameters

Technical improvements:
- Add modifier polling (50ms interval) during window drag to detect Shift key press/release
- Use Mainloop.idle_add to defer window resize operations
- Remove debug logging for production use
- Improve error handling and cleanup in destroy() method

The extension now fully supports:
- Shift-drag window snapping with visual zone overlay
- Keyboard shortcuts for direct zone snapping (Super+Ctrl+1-9)
- Zone overlay toggle (Super+Z)
- Layout cycling (Super+Shift+Z)
- Graphical zone editor (Super+Shift+E)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 19:50:23 -07:00
parent 1681580bea
commit 3d79b0336a
3 changed files with 110 additions and 56 deletions

View File

@@ -31,7 +31,7 @@ A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by
- **Super + Z**: Toggle zone overlay (show/hide zones) - **Super + Z**: Toggle zone overlay (show/hide zones)
- **Super + Shift + Z**: Cycle through different layouts - **Super + Shift + Z**: Cycle through different layouts
- **Super + Shift + E**: Open graphical zone editor - **Super + Shift + E**: Open graphical zone editor
- **Super + Numpad 1-9**: Snap focused window to zone 1-9 - **Super + Ctrl + 1-9**: Snap focused window to zone 1-9
### Mouse Usage ### Mouse Usage

View File

@@ -333,8 +333,9 @@ ZoneManager.prototype = {
this.layouts = DEFAULT_LAYOUTS; this.layouts = DEFAULT_LAYOUTS;
this.isShowing = false; this.isShowing = false;
this.dragInProgress = false; this.dragInProgress = false;
this.draggedWindow = null;
this.editor = new ZoneEditor(); this.editor = new ZoneEditor();
// Connect to window grab events // Connect to window grab events
this._connectSignals(); this._connectSignals();
this._setupKeybindings(); this._setupKeybindings();
@@ -342,10 +343,13 @@ ZoneManager.prototype = {
_connectSignals: function() { _connectSignals: function() {
// Monitor for window grab begin/end // Monitor for window grab begin/end
this.grabOpBeginId = global.display.connect('grab-op-begin', this.grabOpBeginId = global.display.connect('grab-op-begin',
Lang.bind(this, this._onGrabBegin)); Lang.bind(this, this._onGrabBegin));
this.grabOpEndId = global.display.connect('grab-op-end', this.grabOpEndId = global.display.connect('grab-op-end',
Lang.bind(this, this._onGrabEnd)); Lang.bind(this, this._onGrabEnd));
// Monitor for modifier key changes
this.modifierPollTimeoutId = null;
}, },
_setupKeybindings: function() { _setupKeybindings: function() {
@@ -355,46 +359,76 @@ ZoneManager.prototype = {
'<Super>z', '<Super>z',
Lang.bind(this, this._toggleZonesOverlay) Lang.bind(this, this._toggleZonesOverlay)
); );
// Add keybinding to open zone editor (Super+Shift+E) // Add keybinding to open zone editor (Super+Shift+E)
Main.keybindingManager.addHotKey( Main.keybindingManager.addHotKey(
'open-zone-editor', 'open-zone-editor',
'<Super><Shift>e', '<Super><Shift>e',
Lang.bind(this, function() { this.editor.startEditor(); }) Lang.bind(this, function() { this.editor.startEditor(); })
); );
// Add keybindings for quick snap to zones (Super+Numpad) // Add keybindings for quick snap to zones (Super+Ctrl+1-9)
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
const zoneIndex = i - 1; // Capture the value for this iteration
Main.keybindingManager.addHotKey( Main.keybindingManager.addHotKey(
'snap-to-zone-' + i, 'snap-to-zone-' + i,
'<Super>KP_' + i, '<Super><Ctrl>' + i,
Lang.bind(this, function() { this._snapToZone(i - 1); }) Lang.bind(this, function() {
this._snapToZone(zoneIndex);
})
); );
} }
}, },
_onGrabBegin: function(display, window, op) { _checkModifiersDuringDrag: function() {
// Check if this is a window move operation if (!this.dragInProgress) {
if (op === Meta.GrabOp.MOVING) { return false; // Stop polling
this.dragInProgress = true;
// Check if Shift is held - show overlay
let mods = global.get_pointer()[2];
if (mods & Clutter.ModifierType.SHIFT_MASK) {
this._showZonesOverlay();
}
} }
let pointerInfo = global.get_pointer();
let mods = pointerInfo[2];
let shiftPressed = !!(mods & Clutter.ModifierType.SHIFT_MASK);
if (shiftPressed && !this.isShowing) {
this._showZonesOverlay();
} else if (!shiftPressed && this.isShowing) {
this._hideZonesOverlay();
}
return true; // Continue polling
}, },
_onGrabEnd: function(display, window, op) { _onGrabBegin: function(display, window) {
if (op === Meta.GrabOp.MOVING && this.dragInProgress) { // Window drag has begun
this.dragInProgress = true;
// Get the actual window being dragged - use focus_window as it's the dragged window
this.draggedWindow = global.display.focus_window;
// Start polling for modifier key changes during drag
if (this.modifierPollTimeoutId) {
Mainloop.source_remove(this.modifierPollTimeoutId);
}
this.modifierPollTimeoutId = Mainloop.timeout_add(50, Lang.bind(this, this._checkModifiersDuringDrag));
},
_onGrabEnd: function(display, window) {
if (this.dragInProgress) {
this.dragInProgress = false; this.dragInProgress = false;
// Stop polling for modifiers
if (this.modifierPollTimeoutId) {
Mainloop.source_remove(this.modifierPollTimeoutId);
this.modifierPollTimeoutId = null;
}
if (this.isShowing) { if (this.isShowing) {
// Snap to zone if cursor is over one // Snap to zone if cursor is over one
this._snapWindowToZone(window); this._snapWindowToZone(this.draggedWindow);
this._hideZonesOverlay(); this._hideZonesOverlay();
} }
this.draggedWindow = null;
} }
}, },
@@ -489,18 +523,18 @@ ZoneManager.prototype = {
_snapWindowToZone: function(window) { _snapWindowToZone: function(window) {
if (!window) return; if (!window) return;
let [x, y] = global.get_pointer(); let [x, y] = global.get_pointer();
let monitor = Main.layoutManager.primaryMonitor; let monitor = Main.layoutManager.primaryMonitor;
// Convert to relative coordinates // Convert to relative coordinates
let relX = (x - monitor.x) / monitor.width; let relX = (x - monitor.x) / monitor.width;
let relY = (y - monitor.y) / monitor.height; let relY = (y - monitor.y) / monitor.height;
// Find which zone the cursor is in // Find which zone the cursor is in
let layout = this.layouts[this.currentLayout]; let layout = this.layouts[this.currentLayout];
let targetZone = null; let targetZone = null;
for (let zone of layout.zones) { for (let zone of layout.zones) {
if (relX >= zone.x && relX < zone.x + zone.width && if (relX >= zone.x && relX < zone.x + zone.width &&
relY >= zone.y && relY < zone.y + zone.height) { relY >= zone.y && relY < zone.y + zone.height) {
@@ -508,7 +542,7 @@ ZoneManager.prototype = {
break; break;
} }
} }
if (targetZone) { if (targetZone) {
this._moveWindowToZone(window, targetZone, monitor); this._moveWindowToZone(window, targetZone, monitor);
} }
@@ -518,28 +552,35 @@ ZoneManager.prototype = {
// Snap the focused window to a specific zone // Snap the focused window to a specific zone
let window = global.display.focus_window; let window = global.display.focus_window;
if (!window) return; if (!window) return;
let layout = this.layouts[this.currentLayout]; let layout = this.layouts[this.currentLayout];
if (zoneIndex >= layout.zones.length) return; if (zoneIndex >= layout.zones.length) return;
let monitor = Main.layoutManager.primaryMonitor; let monitor = Main.layoutManager.primaryMonitor;
this._moveWindowToZone(window, layout.zones[zoneIndex], monitor); this._moveWindowToZone(window, layout.zones[zoneIndex], monitor);
}, },
_moveWindowToZone: function(window, zone, monitor) { _moveWindowToZone: function(window, zone, monitor) {
if (!window || !zone || !monitor) return;
// Unmaximize if maximized // Unmaximize if maximized
if (window.get_maximized()) { if (window.maximized_horizontally || window.maximized_vertically) {
window.unmaximize(Meta.MaximizeFlags.BOTH); window.unmaximize(Meta.MaximizeFlags.BOTH);
} }
// Calculate absolute coordinates // Calculate absolute coordinates
let x = monitor.x + Math.round(monitor.width * zone.x); let x = monitor.x + Math.round(monitor.width * zone.x);
let y = monitor.y + Math.round(monitor.height * zone.y); let y = monitor.y + Math.round(monitor.height * zone.y);
let width = Math.round(monitor.width * zone.width); let width = Math.round(monitor.width * zone.width);
let height = Math.round(monitor.height * zone.height); let height = Math.round(monitor.height * zone.height);
// Move and resize window // Use Mainloop.idle_add to defer the operation
window.move_resize_frame(true, x, y, width, height); // This ensures the unmaximize completes before move/resize
Mainloop.idle_add(Lang.bind(this, function() {
// Move and resize window (false = programmatic operation, not user-initiated)
window.move_resize_frame(false, x, y, width, height);
return false; // Don't repeat
}));
}, },
cycleLayout: function() { cycleLayout: function() {
@@ -558,28 +599,40 @@ ZoneManager.prototype = {
}, },
destroy: function() { destroy: function() {
// Disconnect signals // Clean up overlay FIRST to prevent corrupt display
if (this.grabOpBeginId) {
global.display.disconnect(this.grabOpBeginId);
}
if (this.grabOpEndId) {
global.display.disconnect(this.grabOpEndId);
}
// Remove keybindings
Main.keybindingManager.removeHotKey('show-zones');
Main.keybindingManager.removeHotKey('open-zone-editor');
for (let i = 1; i <= 9; i++) {
Main.keybindingManager.removeHotKey('snap-to-zone-' + i);
}
// Clean up overlay
this._hideZonesOverlay(); this._hideZonesOverlay();
// Stop modifier polling if active
if (this.modifierPollTimeoutId) {
Mainloop.source_remove(this.modifierPollTimeoutId);
this.modifierPollTimeoutId = null;
}
// Clean up editor // Clean up editor
if (this.editor) { if (this.editor) {
this.editor._cancelEditor(); this.editor._cancelEditor();
} }
// Disconnect signals
if (this.grabOpBeginId) {
global.display.disconnect(this.grabOpBeginId);
this.grabOpBeginId = null;
}
if (this.grabOpEndId) {
global.display.disconnect(this.grabOpEndId);
this.grabOpEndId = null;
}
// Remove keybindings
try {
Main.keybindingManager.removeHotKey('show-zones');
Main.keybindingManager.removeHotKey('open-zone-editor');
for (let i = 1; i <= 9; i++) {
Main.keybindingManager.removeHotKey('snap-to-zone-' + i);
}
} catch(e) {
global.logError('GridSnap: Error removing keybindings - ' + e);
}
} }
}; };

View File

@@ -27,8 +27,9 @@ echo "2. Open System Settings → Extensions"
echo "3. Find 'GridSnap' and toggle it on" echo "3. Find 'GridSnap' and toggle it on"
echo "" echo ""
echo "Keyboard shortcuts:" echo "Keyboard shortcuts:"
echo " Super+Z : Show/hide zones" echo " Super+Z : Show/hide zones"
echo " Super+Shift+Z : Cycle layouts" echo " Super+Shift+Z : Cycle layouts"
echo " Super+Numpad1-9 : Snap to zone" echo " Super+Ctrl+1-9 : Snap to zone"
echo " Super+Shift+E : Open zone editor"
echo "" echo ""
echo "Enjoy your new window manager!" echo "Enjoy your new window manager!"