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:
35
README.md
35
README.md
@@ -5,7 +5,8 @@ A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by
|
||||
## Features
|
||||
|
||||
- **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
|
||||
- **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 + Shift + Z**: Cycle through different layouts
|
||||
- **Super + Shift + E**: Open graphical zone editor
|
||||
- **Super + Numpad 1-9**: Snap focused window to zone 1-9
|
||||
|
||||
### 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
|
||||
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
|
||||
|
||||
1. **2x2 Grid**: Four equal quadrants
|
||||
@@ -45,7 +60,16 @@ A window tiling manager extension for Linux Mint's Cinnamon Desktop, inspired by
|
||||
|
||||
## 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
|
||||
const DEFAULT_LAYOUTS = {
|
||||
@@ -107,13 +131,14 @@ tail -f ~/.cinnamon/glass.log
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Custom layout editor GUI
|
||||
- [x] Custom layout editor GUI
|
||||
- [ ] 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
|
||||
- [ ] More pre-configured layouts
|
||||
- [ ] Animation effects
|
||||
- [ ] Settings panel for configuration
|
||||
- [ ] Settings panel for keybinding customization
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
299
extension.js
299
extension.js
@@ -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() {
|
||||
this._init();
|
||||
}
|
||||
@@ -48,6 +333,7 @@ ZoneManager.prototype = {
|
||||
this.layouts = DEFAULT_LAYOUTS;
|
||||
this.isShowing = false;
|
||||
this.dragInProgress = false;
|
||||
this.editor = new ZoneEditor();
|
||||
|
||||
// Connect to window grab events
|
||||
this._connectSignals();
|
||||
@@ -70,6 +356,13 @@ ZoneManager.prototype = {
|
||||
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)
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
Main.keybindingManager.addHotKey(
|
||||
@@ -275,12 +568,18 @@ ZoneManager.prototype = {
|
||||
|
||||
// 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();
|
||||
|
||||
// Clean up editor
|
||||
if (this.editor) {
|
||||
this.editor._cancelEditor();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,3 +20,28 @@
|
||||
font-weight: bold;
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user