Initial commit - GridSnap Cinnamon Extension
This commit is contained in:
308
extension.js
Normal file
308
extension.js
Normal file
@@ -0,0 +1,308 @@
|
||||
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',
|
||||
'<Super>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,
|
||||
'<Super>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',
|
||||
'<Super><Shift>z',
|
||||
Lang.bind(zoneManager, zoneManager.cycleLayout)
|
||||
);
|
||||
}
|
||||
|
||||
function disable() {
|
||||
if (zoneManager) {
|
||||
Main.keybindingManager.removeHotKey('cycle-layout');
|
||||
zoneManager.destroy();
|
||||
zoneManager = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user