From 1681580bea2caa2958d795c528d48b2ba0e72d56 Mon Sep 17 00:00:00 2001 From: ksmith Date: Fri, 16 Jan 2026 01:49:09 +0000 Subject: [PATCH] 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 --- README.md | 35 +++++- extension.js | 299 +++++++++++++++++++++++++++++++++++++++++++++++++ stylesheet.css | 25 +++++ 3 files changed, 354 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 49b1604..b18faea 100644 --- a/README.md +++ b/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 diff --git a/extension.js b/extension.js index d400919..4d7e9f3 100644 --- a/extension.js +++ b/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', + '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(); + } } }; diff --git a/stylesheet.css b/stylesheet.css index ebcf63d..2185e90 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -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); +}