From 5cef19690ace7bec3d49ce17ea6116919e29bdb6 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Thu, 15 Jan 2026 21:22:58 -0700 Subject: [PATCH] Implement zone dimension display and zone repositioning in editor Feature #15 - Show Zone Dimensions: - Display real-time dimensions while drawing zones - Show width x height in both pixels and percentages - Display position coordinates (x, y) - Dimension label follows cursor with 15px offset - Label positioned to stay within monitor bounds - Automatically cleaned up when zone drawing completes Feature #16 - Move/Reposition Zones: - Click on existing zones to select and move them - Yellow highlight when zone is being moved (vs green for new zones) - Drag zones to new positions while maintaining size - Constrain movement to monitor bounds (can't move outside screen) - Real-time update of zone coordinates in data structure - Green color restored after move completes Editor Improvements: - Added zoneActors array to track zone widgets for moving - Updated instruction label to mention zone moving - Enhanced _findZoneAtPosition() helper to detect clicks on zones - Improved _removeLastZone() to properly clean up zone actors - Better cleanup in _cancelEditor() to prevent memory leaks - Track moving state (isMovingZone, movingZoneIndex, moveOffsetX/Y) UX Enhancements: - Separate visual feedback for creating (green) vs moving (yellow) zones - Smooth drag experience with proper offset tracking - Dimensions update in real-time during zone creation - Clear visual distinction between zone operations Fixes TODO items #15 and #16 Co-Authored-By: Claude Sonnet 4.5 --- TODO.md | 26 +++--- extension.js | 234 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 203 insertions(+), 57 deletions(-) diff --git a/TODO.md b/TODO.md index 9eadf1a..9f6f8c7 100644 --- a/TODO.md +++ b/TODO.md @@ -43,22 +43,22 @@ - [ ] Add UI to choose which layout to edit - [ ] Pre-populate editor with existing zones when editing -### 15. Show Zone Dimensions in Editor +### 15. Show Zone Dimensions in Editor ✅ COMPLETED **Priority: Medium** -- [ ] Display zone dimensions while drawing in graphical editor -- [ ] Show width and height in pixels and/or percentage -- [ ] Update dimensions in real-time as zone is resized -- [ ] Display position coordinates (x, y) -- [ ] Show dimensions near cursor or inside zone preview +- [x] Display zone dimensions while drawing in graphical editor +- [x] Show width and height in pixels and/or percentage +- [x] Update dimensions in real-time as zone is resized +- [x] Display position coordinates (x, y) +- [x] Show dimensions near cursor or inside zone preview -### 16. Move/Reposition Zones in Editor +### 16. Move/Reposition Zones in Editor ✅ COMPLETED **Priority: Medium** -- [ ] Allow clicking and dragging existing zones to move them -- [ ] Visual feedback when zone is selected for moving -- [ ] Snap to grid or guides (optional) -- [ ] Prevent moving zones outside monitor bounds -- [ ] Update zone coordinates as zone is moved -- [ ] Maintain zone size while moving (only change position) +- [x] Allow clicking and dragging existing zones to move them +- [x] Visual feedback when zone is selected for moving (yellow highlight) +- [x] Prevent moving zones outside monitor bounds +- [x] Update zone coordinates as zone is moved +- [x] Maintain zone size while moving (only change position) +- [ ] Snap to grid or guides (future enhancement) ## Code Quality & Robustness diff --git a/extension.js b/extension.js index 4181836..2923582 100644 --- a/extension.js +++ b/extension.js @@ -52,10 +52,16 @@ ZoneEditor.prototype = { this.editorOverlay = null; this.isEditing = false; this.zones = []; + this.zoneActors = []; // Track zone actors for moving this.currentZone = null; + this.dimensionLabel = null; this.startX = 0; this.startY = 0; this.isDragging = false; + this.isMovingZone = false; + this.movingZoneIndex = -1; + this.moveOffsetX = 0; + this.moveOffsetY = 0; }, startEditor: function() { @@ -82,7 +88,7 @@ ZoneEditor.prototype = { // 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', + 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', style_class: 'gridsnap-editor-instructions', x: 20, y: 20 @@ -149,59 +155,172 @@ ZoneEditor.prototype = { _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); - + + // 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; + + // 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); + + // Create dimension label + 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); + } + return Clutter.EVENT_STOP; }, + + _findZoneAtPosition: function(x, y) { + // Check if click is inside any existing zone + for (let i = 0; i < this.zoneActors.length; i++) { + let actor = this.zoneActors[i]; + if (x >= actor.x && x <= actor.x + actor.width && + y >= actor.y && y <= actor.y + actor.height) { + return i; + } + } + return -1; + }, _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; + + if (this.isMovingZone) { + // Moving existing zone + let newX = x - monitor.x - this.moveOffsetX; + let newY = y - monitor.y - this.moveOffsetY; + + let zoneActor = this.zoneActors[this.movingZoneIndex]; + + // Constrain to monitor bounds + newX = Math.max(0, Math.min(newX, monitor.width - zoneActor.width)); + newY = Math.max(0, Math.min(newY, monitor.height - zoneActor.height)); + + zoneActor.set_position(newX, newY); + + // Update zone data + this.zones[this.movingZoneIndex].x = newX / monitor.width; + this.zones[this.movingZoneIndex].y = newY / monitor.height; + + return Clutter.EVENT_STOP; + } + + if (this.isDragging && this.currentZone) { + // Creating new zone + 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); + + // Update dimension label + if (this.dimensionLabel) { + let widthPx = Math.round(rectWidth); + let heightPx = Math.round(rectHeight); + let widthPct = Math.round((rectWidth / monitor.width) * 100); + let heightPct = Math.round((rectHeight / monitor.height) * 100); + + this.dimensionLabel.set_text( + widthPx + 'x' + heightPx + 'px (' + widthPct + '% × ' + heightPct + '%)\n' + + 'Position: ' + Math.round(rectX) + ', ' + Math.round(rectY) + ); + + // Position label near cursor + this.dimensionLabel.set_position( + Math.min(endX + 15, monitor.width - 200), + Math.min(endY + 15, monitor.height - 60) + ); + } + + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; }, _onButtonRelease: function(actor, event) { + if (this.isMovingZone) { + // Finish moving zone + let zoneActor = this.zoneActors[this.movingZoneIndex]; + + // 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;' + ); + + this.isMovingZone = false; + this.movingZoneIndex = -1; + return Clutter.EVENT_STOP; + } + if (!this.isDragging) return Clutter.EVENT_PROPAGATE; - + this.isDragging = false; - + + // Clean up dimension label + if (this.dimensionLabel) { + this.editorOverlay.remove_child(this.dimensionLabel); + this.dimensionLabel.destroy(); + this.dimensionLabel = null; + } + 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 @@ -211,9 +330,12 @@ ZoneEditor.prototype = { width: width / monitor.width, height: height / monitor.height }; - + this.zones.push(zone); - + + // Track the zone actor for moving later + this.zoneActors.push(this.currentZone); + // Add zone number label let label = new St.Label({ text: String(this.zones.length), @@ -242,12 +364,21 @@ ZoneEditor.prototype = { _removeLastZone: function() { if (this.zones.length > 0) { this.zones.pop(); + + // Also remove the last zone actor + if (this.zoneActors.length > 0) { + let removedActor = this.zoneActors.pop(); + this.editorOverlay.remove_child(removedActor); + removedActor.destroy(); + } + // Redraw editor this._cancelEditor(); this.startEditor(); - + // Re-add existing zones let monitor = Main.layoutManager.primaryMonitor; + this.zoneActors = []; // Reset actors array this.zones.forEach((zone, index) => { let zoneActor = new St.Widget({ style_class: 'gridsnap-editor-zone', @@ -261,7 +392,7 @@ ZoneEditor.prototype = { 'background-color: rgba(100, 255, 100, 0.3);' + 'border-radius: 4px;' ); - + let label = new St.Label({ text: String(index + 1), x: 10, @@ -275,6 +406,9 @@ ZoneEditor.prototype = { ); zoneActor.add_child(label); this.editorOverlay.add_child(zoneActor); + + // Track zone actors for moving + this.zoneActors.push(zoneActor); }); } }, @@ -309,22 +443,34 @@ ZoneEditor.prototype = { _cancelEditor: function() { if (!this.isEditing) return; - + + // Clean up dimension label if it exists + if (this.dimensionLabel) { + if (this.editorOverlay) { + this.editorOverlay.remove_child(this.dimensionLabel); + } + this.dimensionLabel.destroy(); + this.dimensionLabel = null; + } + 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.zoneActors = []; this.currentZone = null; + this.isMovingZone = false; + this.movingZoneIndex = -1; } };