Add graphical zone editor and update documentation

- Implemented visual zone editor (Super+Shift+E)
- Draw zones with mouse click-and-drag
- Ctrl+S to save, Ctrl+C to cancel, Delete to remove last zone
- Updated README with zone editor documentation
- Added CSS styles for editor interface
- Custom layouts are now created without code editing
This commit is contained in:
ksmith
2026-01-16 01:49:09 +00:00
parent 51b7de23fa
commit 1681580bea
3 changed files with 354 additions and 5 deletions

View File

@@ -5,7 +5,8 @@ A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by
## Features ## Features
- **Multiple Zone Layouts**: Pre-configured layouts including 2x2 grid, 3 columns, and focus layouts - **Multiple Zone Layouts**: Pre-configured layouts including 2x2 grid, 3 columns, and focus layouts
- **Visual Overlay**: See your zones while dragging windows - **Graphical Zone Editor**: Draw custom zones visually with your mouse - no code editing required!
- **Visual Overlay**: See your zones while dragging windows (hold Shift)
- **Keyboard Shortcuts**: Quick snap to zones using hotkeys - **Keyboard Shortcuts**: Quick snap to zones using hotkeys
- **Layout Cycling**: Switch between different zone layouts on the fly - **Layout Cycling**: Switch between different zone layouts on the fly
@@ -29,6 +30,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 + Numpad 1-9**: Snap focused window to zone 1-9 - **Super + Numpad 1-9**: Snap focused window to zone 1-9
### Mouse Usage ### Mouse Usage
@@ -37,6 +39,19 @@ A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by
2. Drop the window over a zone to snap it there 2. Drop the window over a zone to snap it there
3. Release Shift or move outside zones to cancel 3. Release Shift or move outside zones to cancel
### Graphical Zone Editor
Create custom layouts visually by drawing zones with your mouse:
1. Press **Super + Shift + E** to open the zone editor
2. **Click and drag** to draw rectangular zones
3. Draw as many zones as you need
4. **Ctrl + S** to save your custom layout
5. **Ctrl + C** or **Escape** to cancel
6. **Delete/Backspace** to remove the last zone
Your custom layout will be added to the layout rotation and can be accessed with Super + Shift + Z.
## Available Layouts ## Available Layouts
1. **2x2 Grid**: Four equal quadrants 1. **2x2 Grid**: Four equal quadrants
@@ -45,7 +60,16 @@ A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by
## Customization ## Customization
To add your own layouts, edit the `DEFAULT_LAYOUTS` object in `extension.js`: ### Easy Way: Use the Graphical Editor
1. Press **Super + Shift + E** to open the zone editor
2. Draw zones by clicking and dragging
3. Press **Ctrl + S** to save your custom layout
4. Your layout is immediately available in the layout rotation!
### Advanced Way: Edit the Code
To add your own layouts manually, edit the `DEFAULT_LAYOUTS` object in `extension.js`:
```javascript ```javascript
const DEFAULT_LAYOUTS = { const DEFAULT_LAYOUTS = {
@@ -107,13 +131,14 @@ tail -f ~/.cinnamon/glass.log
## Future Enhancements ## Future Enhancements
- [ ] Custom layout editor GUI - [x] Custom layout editor GUI
- [ ] Per-monitor zone configurations - [ ] Per-monitor zone configurations
- [ ] Save window positions and restore on login - [ ] Save custom layouts permanently to file
- [ ] Edit existing custom layouts in the graphical editor
- [ ] Zone layout import/export - [ ] Zone layout import/export
- [ ] More pre-configured layouts - [ ] More pre-configured layouts
- [ ] Animation effects - [ ] Animation effects
- [ ] Settings panel for configuration - [ ] Settings panel for keybinding customization
## Contributing ## Contributing

View File

@@ -37,6 +37,291 @@ const DEFAULT_LAYOUTS = {
} }
}; };
function ZoneEditor() {
this._init();
}
ZoneEditor.prototype = {
_init: function() {
this.editorOverlay = null;
this.isEditing = false;
this.zones = [];
this.currentZone = null;
this.startX = 0;
this.startY = 0;
this.isDragging = false;
},
startEditor: function() {
if (this.isEditing) return;
this.isEditing = true;
this.zones = [];
let monitor = Main.layoutManager.primaryMonitor;
// Create editor overlay
this.editorOverlay = new St.Widget({
style_class: 'gridsnap-editor-overlay',
reactive: true,
x: monitor.x,
y: monitor.y,
width: monitor.width,
height: monitor.height
});
this.editorOverlay.set_style(
'background-color: rgba(0, 0, 0, 0.5);'
);
// Add instruction label
this.instructionLabel = new St.Label({
text: 'GridSnap Zone Editor\n\nDrag to draw zones | Ctrl+S to save | Ctrl+C to cancel | Delete to clear last zone',
style_class: 'gridsnap-editor-instructions',
x: 20,
y: 20
});
this.instructionLabel.set_style(
'color: white;' +
'font-size: 18px;' +
'background-color: rgba(0, 0, 0, 0.8);' +
'padding: 15px;' +
'border-radius: 8px;'
);
this.editorOverlay.add_child(this.instructionLabel);
// Connect mouse events
this.buttonPressId = this.editorOverlay.connect('button-press-event',
Lang.bind(this, this._onButtonPress));
this.buttonReleaseId = this.editorOverlay.connect('button-release-event',
Lang.bind(this, this._onButtonRelease));
this.motionId = this.editorOverlay.connect('motion-event',
Lang.bind(this, this._onMotion));
Main.layoutManager.addChrome(this.editorOverlay);
Main.pushModal(this.editorOverlay);
// Setup key listener for save/cancel
this._setupEditorKeys();
},
_setupEditorKeys: function() {
this.keyPressId = this.editorOverlay.connect('key-press-event',
Lang.bind(this, function(actor, event) {
let symbol = event.get_key_symbol();
let state = event.get_state();
// Ctrl+S to save
if (symbol === Clutter.KEY_s && (state & Clutter.ModifierType.CONTROL_MASK)) {
this._saveLayout();
return Clutter.EVENT_STOP;
}
// Ctrl+C to cancel
if (symbol === Clutter.KEY_c && (state & Clutter.ModifierType.CONTROL_MASK)) {
this._cancelEditor();
return Clutter.EVENT_STOP;
}
// Delete to remove last zone
if (symbol === Clutter.KEY_Delete || symbol === Clutter.KEY_BackSpace) {
this._removeLastZone();
return Clutter.EVENT_STOP;
}
// Escape to cancel
if (symbol === Clutter.KEY_Escape) {
this._cancelEditor();
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
})
);
},
_onButtonPress: function(actor, event) {
let [x, y] = event.get_coords();
let monitor = Main.layoutManager.primaryMonitor;
this.startX = x - monitor.x;
this.startY = y - monitor.y;
this.isDragging = true;
// Create zone preview
this.currentZone = new St.Widget({
style_class: 'gridsnap-editor-zone',
x: this.startX,
y: this.startY,
width: 1,
height: 1
});
this.currentZone.set_style(
'border: 3px solid rgba(100, 255, 100, 0.9);' +
'background-color: rgba(100, 255, 100, 0.3);' +
'border-radius: 4px;'
);
this.editorOverlay.add_child(this.currentZone);
return Clutter.EVENT_STOP;
},
_onMotion: function(actor, event) {
if (!this.isDragging || !this.currentZone) return Clutter.EVENT_PROPAGATE;
let [x, y] = event.get_coords();
let monitor = Main.layoutManager.primaryMonitor;
let endX = x - monitor.x;
let endY = y - monitor.y;
let rectX = Math.min(this.startX, endX);
let rectY = Math.min(this.startY, endY);
let rectWidth = Math.abs(endX - this.startX);
let rectHeight = Math.abs(endY - this.startY);
this.currentZone.set_position(rectX, rectY);
this.currentZone.set_size(rectWidth, rectHeight);
return Clutter.EVENT_STOP;
},
_onButtonRelease: function(actor, event) {
if (!this.isDragging) return Clutter.EVENT_PROPAGATE;
this.isDragging = false;
if (this.currentZone) {
let monitor = Main.layoutManager.primaryMonitor;
let width = this.currentZone.width;
let height = this.currentZone.height;
// Only save zones with reasonable size
if (width > 20 && height > 20) {
// Convert to relative coordinates
let zone = {
x: this.currentZone.x / monitor.width,
y: this.currentZone.y / monitor.height,
width: width / monitor.width,
height: height / monitor.height
};
this.zones.push(zone);
// Add zone number label
let label = new St.Label({
text: String(this.zones.length),
style_class: 'gridsnap-editor-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);'
);
this.currentZone.add_child(label);
} else {
// Remove if too small
this.currentZone.destroy();
}
this.currentZone = null;
}
return Clutter.EVENT_STOP;
},
_removeLastZone: function() {
if (this.zones.length > 0) {
this.zones.pop();
// Redraw editor
this._cancelEditor();
this.startEditor();
// Re-add existing zones
let monitor = Main.layoutManager.primaryMonitor;
this.zones.forEach((zone, index) => {
let zoneActor = new St.Widget({
style_class: 'gridsnap-editor-zone',
x: zone.x * monitor.width,
y: zone.y * monitor.height,
width: zone.width * monitor.width,
height: zone.height * monitor.height
});
zoneActor.set_style(
'border: 3px solid rgba(100, 255, 100, 0.9);' +
'background-color: rgba(100, 255, 100, 0.3);' +
'border-radius: 4px;'
);
let label = new St.Label({
text: String(index + 1),
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);'
);
zoneActor.add_child(label);
this.editorOverlay.add_child(zoneActor);
});
}
},
_saveLayout: function() {
if (this.zones.length === 0) {
Main.notify('GridSnap', 'No zones to save!');
this._cancelEditor();
return;
}
// Generate layout name
let layoutId = 'custom-' + Date.now();
let layoutName = 'Custom Layout (' + this.zones.length + ' zones)';
// Add to zoneManager's layouts
if (zoneManager) {
zoneManager.layouts[layoutId] = {
name: layoutName,
zones: this.zones
};
zoneManager.currentLayout = layoutId;
Main.notify('GridSnap', 'Layout saved: ' + layoutName);
// TODO: Persist to file for permanent storage
global.log('GridSnap: Layout saved - ' + JSON.stringify(zoneManager.layouts[layoutId]));
}
this._cancelEditor();
},
_cancelEditor: function() {
if (!this.isEditing) return;
if (this.editorOverlay) {
if (this.buttonPressId) this.editorOverlay.disconnect(this.buttonPressId);
if (this.buttonReleaseId) this.editorOverlay.disconnect(this.buttonReleaseId);
if (this.motionId) this.editorOverlay.disconnect(this.motionId);
if (this.keyPressId) this.editorOverlay.disconnect(this.keyPressId);
Main.popModal(this.editorOverlay);
Main.layoutManager.removeChrome(this.editorOverlay);
this.editorOverlay.destroy();
this.editorOverlay = null;
}
this.isEditing = false;
this.zones = [];
this.currentZone = null;
}
};
function ZoneManager() { function ZoneManager() {
this._init(); this._init();
} }
@@ -48,6 +333,7 @@ ZoneManager.prototype = {
this.layouts = DEFAULT_LAYOUTS; this.layouts = DEFAULT_LAYOUTS;
this.isShowing = false; this.isShowing = false;
this.dragInProgress = false; this.dragInProgress = false;
this.editor = new ZoneEditor();
// Connect to window grab events // Connect to window grab events
this._connectSignals(); this._connectSignals();
@@ -70,6 +356,13 @@ ZoneManager.prototype = {
Lang.bind(this, this._toggleZonesOverlay) Lang.bind(this, this._toggleZonesOverlay)
); );
// Add keybinding to open zone editor (Super+Shift+E)
Main.keybindingManager.addHotKey(
'open-zone-editor',
'<Super><Shift>e',
Lang.bind(this, function() { this.editor.startEditor(); })
);
// Add keybindings for quick snap to zones (Super+Numpad) // Add keybindings for quick snap to zones (Super+Numpad)
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
Main.keybindingManager.addHotKey( Main.keybindingManager.addHotKey(
@@ -275,12 +568,18 @@ ZoneManager.prototype = {
// Remove keybindings // Remove keybindings
Main.keybindingManager.removeHotKey('show-zones'); Main.keybindingManager.removeHotKey('show-zones');
Main.keybindingManager.removeHotKey('open-zone-editor');
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
Main.keybindingManager.removeHotKey('snap-to-zone-' + i); Main.keybindingManager.removeHotKey('snap-to-zone-' + i);
} }
// Clean up overlay // Clean up overlay
this._hideZonesOverlay(); this._hideZonesOverlay();
// Clean up editor
if (this.editor) {
this.editor._cancelEditor();
}
} }
}; };

View File

@@ -20,3 +20,28 @@
font-weight: bold; font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
} }
.gridsnap-editor-overlay {
background-color: rgba(0, 0, 0, 0.5);
}
.gridsnap-editor-zone {
border: 3px solid rgba(100, 255, 100, 0.9);
background-color: rgba(100, 255, 100, 0.3);
border-radius: 4px;
}
.gridsnap-editor-instructions {
color: white;
font-size: 18px;
background-color: rgba(0, 0, 0, 0.8);
padding: 15px;
border-radius: 8px;
}
.gridsnap-editor-zone-label {
color: white;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}