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 <noreply@anthropic.com>
This commit is contained in:
12
TODO.md
12
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
|
||||
|
||||
214
extension.js
214
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user