Add ability to edit existing custom layouts

Implemented layout editing functionality with selection UI:

Features:
- New layout selector dialog when pressing Super+Shift+E
- Lists all existing custom layouts with zone counts
- "Create New Layout" option for new layouts
- Click any layout to edit it in the zone editor
- Existing zones pre-populated when editing
- Updates existing layout when saving (shows "updated" message)
- ESC key or Cancel button to close selector

Implementation details:
- Added editingLayoutId and editingLayoutName properties to track edit mode
- Modified startEditor() to accept optional layoutId and layoutData parameters
- Created showLayoutSelector() method to display layout selection UI
- Created _createZoneActorsFromData() to pre-populate zones when editing
- Updated _saveLayout() to update existing layout vs create new
- Changed keybinding to call showLayoutSelector() instead of startEditor()
- Added _closeLayoutSelector() for cleanup
- Deep copy of zones array to avoid reference issues

User experience:
- Press Super+Shift+E to open layout selector
- Choose existing layout to edit or create new
- Edit zones (move, resize, add, delete)
- Save updates existing layout or creates new
- Clear visual feedback for edit vs create mode

Fixes TODO item #5.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 21:40:57 -07:00
parent ee2f4792ac
commit 7816d41571
2 changed files with 250 additions and 19 deletions

10
TODO.md
View File

@@ -36,12 +36,12 @@
- [ ] Verify zone calculations work correctly with display scaling
- [ ] May need to use `get_resource_scale()` or similar
### 5. Edit Existing Custom Layouts
### 5. Edit Existing Custom Layouts ✅ COMPLETED
**Priority: Medium**
- [ ] Currently can only create new layouts in editor
- [ ] Add ability to select and edit existing custom layouts
- [ ] Add UI to choose which layout to edit
- [ ] Pre-populate editor with existing zones when editing
- [x] Currently can only create new layouts in editor
- [x] Add ability to select and edit existing custom layouts
- [x] Add UI to choose which layout to edit
- [x] Pre-populate editor with existing zones when editing
### 15. Show Zone Dimensions in Editor ✅ COMPLETED
**Priority: Medium**

View File

@@ -66,13 +66,192 @@ ZoneEditor.prototype = {
this.resizingZoneIndex = -1;
this.resizeEdge = null; // 'n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'
this.originalZoneBounds = null;
this.editingLayoutId = null; // Track if editing existing layout
this.editingLayoutName = null;
this.selectorOverlay = null; // Layout selector UI
},
startEditor: function() {
showLayoutSelector: function() {
if (this.isEditing || this.selectorOverlay) return;
let monitor = Main.layoutManager.primaryMonitor;
// Create selector overlay
this.selectorOverlay = new St.Widget({
style_class: 'gridsnap-selector-overlay',
reactive: true,
x: monitor.x,
y: monitor.y,
width: monitor.width,
height: monitor.height
});
this.selectorOverlay.set_style(
'background-color: rgba(0, 0, 0, 0.7);'
);
// Create container for buttons
let buttonContainer = new St.BoxLayout({
vertical: true,
x: monitor.width / 2 - 200,
y: monitor.height / 2 - 200,
width: 400
});
buttonContainer.set_style(
'background-color: rgba(30, 30, 30, 0.95);' +
'padding: 20px;' +
'border-radius: 10px;' +
'spacing: 10px;'
);
// Add title
let title = new St.Label({
text: 'GridSnap Zone Editor'
});
title.set_style(
'color: white;' +
'font-size: 24px;' +
'font-weight: bold;' +
'margin-bottom: 10px;'
);
buttonContainer.add_child(title);
// Add subtitle
let subtitle = new St.Label({
text: 'Choose layout to edit or create new:'
});
subtitle.set_style(
'color: rgba(255, 255, 255, 0.8);' +
'font-size: 14px;' +
'margin-bottom: 20px;'
);
buttonContainer.add_child(subtitle);
// Add "Create New Layout" button
let newButton = this._createSelectorButton('+ Create New Layout', null, null);
buttonContainer.add_child(newButton);
// Add separator
let separator = new St.Widget({
height: 2
});
separator.set_style(
'background-color: rgba(255, 255, 255, 0.2);' +
'margin-top: 10px;' +
'margin-bottom: 10px;'
);
buttonContainer.add_child(separator);
// Add buttons for existing custom layouts
if (zoneManager && zoneManager.layouts) {
let hasCustomLayouts = false;
for (let layoutId in zoneManager.layouts) {
if (layoutId.startsWith('custom-')) {
hasCustomLayouts = true;
let layout = zoneManager.layouts[layoutId];
let button = this._createSelectorButton(
layout.name + ' (' + layout.zones.length + ' zones)',
layoutId,
layout
);
buttonContainer.add_child(button);
}
}
if (!hasCustomLayouts) {
let noLayoutsLabel = new St.Label({
text: 'No custom layouts yet'
});
noLayoutsLabel.set_style(
'color: rgba(255, 255, 255, 0.5);' +
'font-size: 12px;' +
'font-style: italic;' +
'margin: 10px 0px;'
);
buttonContainer.add_child(noLayoutsLabel);
}
}
// Add cancel button
let cancelButton = new St.Button({
label: 'Cancel'
});
cancelButton.set_style(
'color: white;' +
'background-color: rgba(100, 100, 100, 0.5);' +
'padding: 10px 20px;' +
'border-radius: 5px;' +
'margin-top: 20px;'
);
cancelButton.connect('clicked', Lang.bind(this, function() {
this._closeLayoutSelector();
}));
buttonContainer.add_child(cancelButton);
this.selectorOverlay.add_child(buttonContainer);
// Add key press handler for ESC
this.selectorKeyPressId = this.selectorOverlay.connect('key-press-event',
Lang.bind(this, function(actor, event) {
let symbol = event.get_key_symbol();
if (symbol === Clutter.KEY_Escape) {
this._closeLayoutSelector();
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
})
);
Main.layoutManager.addChrome(this.selectorOverlay);
Main.pushModal(this.selectorOverlay);
},
_createSelectorButton: function(label, layoutId, layoutData) {
let button = new St.Button({
label: label
});
button.set_style(
'color: white;' +
'background-color: rgba(100, 149, 237, 0.5);' +
'padding: 15px 20px;' +
'border-radius: 5px;' +
'text-align: left;'
);
button.connect('clicked', Lang.bind(this, function() {
this._closeLayoutSelector();
this.startEditor(layoutId, layoutData);
}));
return button;
},
_closeLayoutSelector: function() {
if (!this.selectorOverlay) return;
if (this.selectorKeyPressId) {
this.selectorOverlay.disconnect(this.selectorKeyPressId);
}
Main.popModal(this.selectorOverlay);
Main.layoutManager.removeChrome(this.selectorOverlay);
this.selectorOverlay.destroy();
this.selectorOverlay = null;
},
startEditor: function(layoutId, layoutData) {
if (this.isEditing) return;
this.isEditing = true;
// If editing existing layout, load it
if (layoutId && layoutData) {
this.editingLayoutId = layoutId;
this.editingLayoutName = layoutData.name;
this.zones = JSON.parse(JSON.stringify(layoutData.zones)); // Deep copy
} else {
this.editingLayoutId = null;
this.editingLayoutName = null;
this.zones = [];
}
let monitor = Main.layoutManager.primaryMonitor;
@@ -117,10 +296,49 @@ ZoneEditor.prototype = {
Main.layoutManager.addChrome(this.editorOverlay);
Main.pushModal(this.editorOverlay);
// If editing existing layout, create zone actors for existing zones
if (this.editingLayoutId && this.zones.length > 0) {
this._createZoneActorsFromData(monitor);
}
// Setup key listener for save/cancel
this._setupEditorKeys();
},
_createZoneActorsFromData: function(monitor) {
// Create zone actors from existing zone data
this.zoneActors = [];
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);
this.zoneActors.push(zoneActor);
});
},
_setupEditorKeys: function() {
this.keyPressId = this.editorOverlay.connect('key-press-event',
Lang.bind(this, function(actor, event) {
@@ -572,11 +790,20 @@ ZoneEditor.prototype = {
return;
}
// Generate layout name
let layoutId = 'custom-' + Date.now();
let layoutName = 'Custom Layout (' + this.zones.length + ' zones)';
let layoutId, layoutName;
// Add to zoneManager's layouts
// Check if editing existing layout or creating new
if (this.editingLayoutId) {
// Update existing layout
layoutId = this.editingLayoutId;
layoutName = this.editingLayoutName + ' (updated)';
} else {
// Create new layout
layoutId = 'custom-' + Date.now();
layoutName = 'Custom Layout (' + this.zones.length + ' zones)';
}
// Save to zoneManager's layouts
if (zoneManager) {
zoneManager.layouts[layoutId] = {
name: layoutName,
@@ -587,8 +814,12 @@ ZoneEditor.prototype = {
// Persist to file for permanent storage
zoneManager._saveLayouts();
if (this.editingLayoutId) {
Main.notify('GridSnap', 'Layout updated: ' + layoutName);
} else {
Main.notify('GridSnap', 'Layout saved: ' + layoutName);
}
}
this._cancelEditor();
},
@@ -678,7 +909,7 @@ ZoneManager.prototype = {
Main.keybindingManager.addHotKey(
'open-zone-editor',
'<Super><Shift>e',
Lang.bind(this, function() { this.editor.startEditor(); })
Lang.bind(this, function() { this.editor.showLayoutSelector(); })
);
// Add keybindings for quick snap to zones (Super+Ctrl+1-9)