From 83a6a1ee8e8a094a5ae9e4baa237deec7c3fc2b9 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Thu, 15 Jan 2026 21:28:25 -0700 Subject: [PATCH] Add zone resizing to graphical editor Implemented comprehensive zone resizing functionality in the zone editor: Features: - Detects when mouse is near zone edges/corners (10px threshold) - Supports all 8 resize handles: N, S, E, W, NE, NW, SE, SW - Visual feedback with cyan highlight during resize - Real-time dimension label updates while resizing - Enforces minimum zone size (20x20 pixels) - Constrains resizing to monitor boundaries - Maintains zone data structure integrity during resize Implementation details: - Added state tracking: isResizingZone, resizingZoneIndex, resizeEdge, originalZoneBounds - New method _findResizeEdge() for edge/corner detection - Enhanced _onButtonPress() to prioritize resize over move - Extended _onMotion() with resize delta calculations for all 8 directions - Updated instruction label to include resize guidance - Updated TODO.md to mark item #17 as completed The zone editor now supports create, move, and resize operations with clear visual feedback for each mode (green=normal, yellow=moving, cyan=resizing). Co-Authored-By: Claude Sonnet 4.5 --- TODO.md | 12 +++ extension.js | 214 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 213 insertions(+), 13 deletions(-) diff --git a/TODO.md b/TODO.md index 9f6f8c7..e2f3b91 100644 --- a/TODO.md +++ b/TODO.md @@ -60,6 +60,18 @@ - [x] Maintain zone size while moving (only change position) - [ ] Snap to grid or guides (future enhancement) +### 17. Resize Zones in Editor ✅ COMPLETED +**Priority: High** +- [x] Detect mouse near edges and corners of zones (10px threshold) +- [x] Allow dragging edges to resize zones horizontally or vertically +- [x] Allow dragging corners to resize zones diagonally +- [x] Visual feedback when zone is being resized (cyan highlight) +- [x] Show dimension label during resize with real-time updates +- [x] Enforce minimum zone size (20x20 pixels) +- [x] Constrain resize to monitor bounds +- [x] Update zone data structure in real-time during resize +- [x] Support all 8 resize handles (N, S, E, W, NE, NW, SE, SW) + ## Code Quality & Robustness ### 6. Comprehensive Error Handling diff --git a/extension.js b/extension.js index 2923582..b286fbc 100644 --- a/extension.js +++ b/extension.js @@ -62,6 +62,10 @@ ZoneEditor.prototype = { this.movingZoneIndex = -1; this.moveOffsetX = 0; this.moveOffsetY = 0; + this.isResizingZone = false; + this.resizingZoneIndex = -1; + this.resizeEdge = null; // 'n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw' + this.originalZoneBounds = null; }, startEditor: function() { @@ -88,7 +92,7 @@ ZoneEditor.prototype = { // Add instruction label this.instructionLabel = new St.Label({ - text: 'GridSnap Zone Editor\n\nDrag to draw zones | Click zone to move | Ctrl+S to save | Ctrl+C to cancel | Delete to clear last zone', + text: 'GridSnap Zone Editor\n\nDrag to draw zones | Click zone to move | Drag edges to resize | Ctrl+S to save | Ctrl+C to cancel | Delete to clear last zone', style_class: 'gridsnap-editor-instructions', x: 20, y: 20 @@ -159,25 +163,65 @@ ZoneEditor.prototype = { this.startX = x - monitor.x; this.startY = y - monitor.y; - // Check if clicking on an existing zone to move it - let clickedZoneIndex = this._findZoneAtPosition(this.startX, this.startY); + // Check if clicking on a resize edge first (higher priority) + let resizeInfo = this._findResizeEdge(this.startX, this.startY); - if (clickedZoneIndex >= 0) { - // Start moving existing zone - this.isMovingZone = true; - this.movingZoneIndex = clickedZoneIndex; + if (resizeInfo) { + // Start resizing zone + this.isResizingZone = true; + this.resizingZoneIndex = resizeInfo.zoneIndex; + this.resizeEdge = resizeInfo.edge; - let zoneActor = this.zoneActors[clickedZoneIndex]; - this.moveOffsetX = this.startX - zoneActor.x; - this.moveOffsetY = this.startY - zoneActor.y; + let zoneActor = this.zoneActors[resizeInfo.zoneIndex]; - // Highlight zone being moved + // Store original bounds for resizing + this.originalZoneBounds = { + x: zoneActor.x, + y: zoneActor.y, + width: zoneActor.width, + height: zoneActor.height + }; + + // Highlight zone being resized (cyan color) zoneActor.set_style( - 'border: 3px solid rgba(255, 255, 100, 0.9);' + - 'background-color: rgba(255, 255, 100, 0.4);' + + 'border: 3px solid rgba(100, 255, 255, 0.9);' + + 'background-color: rgba(100, 255, 255, 0.4);' + 'border-radius: 4px;' ); + + // Create dimension label for resize feedback + this.dimensionLabel = new St.Label({ + text: '', + style_class: 'gridsnap-dimension-label' + }); + this.dimensionLabel.set_style( + 'color: white;' + + 'font-size: 14px;' + + 'background-color: rgba(0, 0, 0, 0.8);' + + 'padding: 5px 10px;' + + 'border-radius: 4px;' + ); + this.editorOverlay.add_child(this.dimensionLabel); } else { + // Check if clicking on an existing zone to move it + let clickedZoneIndex = this._findZoneAtPosition(this.startX, this.startY); + + if (clickedZoneIndex >= 0) { + // Start moving existing zone + this.isMovingZone = true; + this.movingZoneIndex = clickedZoneIndex; + + let zoneActor = this.zoneActors[clickedZoneIndex]; + this.moveOffsetX = this.startX - zoneActor.x; + this.moveOffsetY = this.startY - zoneActor.y; + + // Highlight zone being moved + zoneActor.set_style( + 'border: 3px solid rgba(255, 255, 100, 0.9);' + + 'background-color: rgba(255, 255, 100, 0.4);' + + 'border-radius: 4px;' + ); + } else { // Start creating new zone this.isDragging = true; @@ -225,11 +269,126 @@ ZoneEditor.prototype = { } return -1; }, + + _findResizeEdge: function(x, y) { + // Detect if mouse is near edge/corner of any zone + // Returns {zoneIndex, edge} or null + const EDGE_THRESHOLD = 10; // pixels from edge to trigger resize + + for (let i = 0; i < this.zoneActors.length; i++) { + let actor = this.zoneActors[i]; + let left = actor.x; + let right = actor.x + actor.width; + let top = actor.y; + let bottom = actor.y + actor.height; + + // Check if inside zone bounds (with some margin) + if (x >= left - EDGE_THRESHOLD && x <= right + EDGE_THRESHOLD && + y >= top - EDGE_THRESHOLD && y <= bottom + EDGE_THRESHOLD) { + + let nearLeft = Math.abs(x - left) <= EDGE_THRESHOLD; + let nearRight = Math.abs(x - right) <= EDGE_THRESHOLD; + let nearTop = Math.abs(y - top) <= EDGE_THRESHOLD; + let nearBottom = Math.abs(y - bottom) <= EDGE_THRESHOLD; + + // Corners have priority + if (nearTop && nearLeft) return {zoneIndex: i, edge: 'nw'}; + if (nearTop && nearRight) return {zoneIndex: i, edge: 'ne'}; + if (nearBottom && nearLeft) return {zoneIndex: i, edge: 'sw'}; + if (nearBottom && nearRight) return {zoneIndex: i, edge: 'se'}; + + // Edges + if (nearTop) return {zoneIndex: i, edge: 'n'}; + if (nearBottom) return {zoneIndex: i, edge: 's'}; + if (nearLeft) return {zoneIndex: i, edge: 'w'}; + if (nearRight) return {zoneIndex: i, edge: 'e'}; + } + } + return null; + }, _onMotion: function(actor, event) { let [x, y] = event.get_coords(); let monitor = Main.layoutManager.primaryMonitor; + if (this.isResizingZone) { + // Resizing existing zone + let currentX = x - monitor.x; + let currentY = y - monitor.y; + let deltaX = currentX - this.startX; + let deltaY = currentY - this.startY; + + let zoneActor = this.zoneActors[this.resizingZoneIndex]; + let newX = this.originalZoneBounds.x; + let newY = this.originalZoneBounds.y; + let newWidth = this.originalZoneBounds.width; + let newHeight = this.originalZoneBounds.height; + + // Apply resize based on which edge/corner is being dragged + const MIN_SIZE = 20; + + if (this.resizeEdge.includes('n')) { + newY = this.originalZoneBounds.y + deltaY; + newHeight = this.originalZoneBounds.height - deltaY; + if (newHeight < MIN_SIZE) { + newHeight = MIN_SIZE; + newY = this.originalZoneBounds.y + this.originalZoneBounds.height - MIN_SIZE; + } + } + if (this.resizeEdge.includes('s')) { + newHeight = this.originalZoneBounds.height + deltaY; + newHeight = Math.max(MIN_SIZE, newHeight); + } + if (this.resizeEdge.includes('w')) { + newX = this.originalZoneBounds.x + deltaX; + newWidth = this.originalZoneBounds.width - deltaX; + if (newWidth < MIN_SIZE) { + newWidth = MIN_SIZE; + newX = this.originalZoneBounds.x + this.originalZoneBounds.width - MIN_SIZE; + } + } + if (this.resizeEdge.includes('e')) { + newWidth = this.originalZoneBounds.width + deltaX; + newWidth = Math.max(MIN_SIZE, newWidth); + } + + // Constrain to monitor bounds + newX = Math.max(0, newX); + newY = Math.max(0, newY); + newWidth = Math.min(newWidth, monitor.width - newX); + newHeight = Math.min(newHeight, monitor.height - newY); + + // Update actor + zoneActor.set_position(newX, newY); + zoneActor.set_size(newWidth, newHeight); + + // Update zone data + this.zones[this.resizingZoneIndex].x = newX / monitor.width; + this.zones[this.resizingZoneIndex].y = newY / monitor.height; + this.zones[this.resizingZoneIndex].width = newWidth / monitor.width; + this.zones[this.resizingZoneIndex].height = newHeight / monitor.height; + + // Update dimension label + if (this.dimensionLabel) { + let widthPx = Math.round(newWidth); + let heightPx = Math.round(newHeight); + let widthPct = Math.round((newWidth / monitor.width) * 100); + let heightPct = Math.round((newHeight / monitor.height) * 100); + + this.dimensionLabel.set_text( + widthPx + 'x' + heightPx + 'px (' + widthPct + '% × ' + heightPct + '%)\n' + + 'Position: ' + Math.round(newX) + ', ' + Math.round(newY) + ); + + this.dimensionLabel.set_position( + Math.min(currentX + 15, monitor.width - 200), + Math.min(currentY + 15, monitor.height - 60) + ); + } + + return Clutter.EVENT_STOP; + } + if (this.isMovingZone) { // Moving existing zone let newX = x - monitor.x - this.moveOffsetX; @@ -289,6 +448,31 @@ ZoneEditor.prototype = { }, _onButtonRelease: function(actor, event) { + if (this.isResizingZone) { + // Finish resizing zone + let zoneActor = this.zoneActors[this.resizingZoneIndex]; + + // Reset highlight + zoneActor.set_style( + 'border: 3px solid rgba(100, 255, 100, 0.9);' + + 'background-color: rgba(100, 255, 100, 0.3);' + + 'border-radius: 4px;' + ); + + // Clean up dimension label + if (this.dimensionLabel) { + this.editorOverlay.remove_child(this.dimensionLabel); + this.dimensionLabel.destroy(); + this.dimensionLabel = null; + } + + this.isResizingZone = false; + this.resizingZoneIndex = -1; + this.resizeEdge = null; + this.originalZoneBounds = null; + return Clutter.EVENT_STOP; + } + if (this.isMovingZone) { // Finish moving zone let zoneActor = this.zoneActors[this.movingZoneIndex]; @@ -471,6 +655,10 @@ ZoneEditor.prototype = { this.currentZone = null; this.isMovingZone = false; this.movingZoneIndex = -1; + this.isResizingZone = false; + this.resizingZoneIndex = -1; + this.resizeEdge = null; + this.originalZoneBounds = null; } };