Implement split-based zone editor and multi-monitor support
Major changes: - Replace free-form drawing with split-based tiling interface - Add per-monitor zone configurations and storage - Implement monitor selector UI for multi-monitor setups - Add H/V keyboard shortcuts for horizontal/vertical splits - Add divider dragging for adjusting zone positions - Display real-time dimensions in center of each zone - Migrate storage format to v2 with per-monitor layouts - Update settings panel for per-monitor layout management Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
12
TODO.md
12
TODO.md
@@ -21,13 +21,13 @@
|
||||
|
||||
## Functionality Improvements
|
||||
|
||||
### 3. Multi-Monitor Support
|
||||
### 3. Multi-Monitor Support ✅ COMPLETED
|
||||
**Priority: High**
|
||||
- [ ] Currently only works on primary monitor (extension.js:61)
|
||||
- [ ] Need per-monitor zone configurations
|
||||
- [ ] Detect which monitor window is on when snapping
|
||||
- [ ] Allow zone editor to work on any monitor
|
||||
- [ ] Store zone layouts per monitor
|
||||
- [x] Currently only works on primary monitor (extension.js:61)
|
||||
- [x] Need per-monitor zone configurations
|
||||
- [x] Detect which monitor window is on when snapping
|
||||
- [x] Allow zone editor to work on any monitor
|
||||
- [x] Store zone layouts per monitor
|
||||
|
||||
### 4. High-DPI Display Support
|
||||
**Priority: Medium**
|
||||
|
||||
1022
extension.js
1022
extension.js
File diff suppressed because it is too large
Load Diff
125
settings.js
125
settings.js
@@ -10,6 +10,21 @@ function init() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
function getMonitorCount() {
|
||||
// Try to detect monitor count from display
|
||||
// Fallback to 1 if we can't detect
|
||||
try {
|
||||
const Gdk = imports.gi.Gdk;
|
||||
let display = Gdk.Display.get_default();
|
||||
if (display) {
|
||||
return display.get_n_monitors();
|
||||
}
|
||||
} catch(e) {
|
||||
log('Could not detect monitor count, defaulting to 1: ' + e);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function buildPrefsWidget() {
|
||||
let frame = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
@@ -36,9 +51,34 @@ function buildPrefsWidget() {
|
||||
});
|
||||
frame.pack_start(layoutsLabel, false, false, 5);
|
||||
|
||||
// List of custom layouts
|
||||
let layoutsList = createLayoutsList();
|
||||
frame.pack_start(layoutsList, true, true, 0);
|
||||
// Monitor selector
|
||||
let monitorBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_bottom: 10
|
||||
});
|
||||
|
||||
let monitorLabel = new Gtk.Label({
|
||||
label: 'Select Monitor:'
|
||||
});
|
||||
monitorBox.pack_start(monitorLabel, false, false, 0);
|
||||
|
||||
let monitorCombo = new Gtk.ComboBoxText();
|
||||
let monitorCount = getMonitorCount();
|
||||
for (let i = 0; i < monitorCount; i++) {
|
||||
let label = i === 0 ? 'Monitor 0 (Primary)' : 'Monitor ' + i;
|
||||
monitorCombo.append_text(label);
|
||||
}
|
||||
monitorCombo.set_active(0);
|
||||
monitorBox.pack_start(monitorCombo, false, false, 0);
|
||||
|
||||
frame.pack_start(monitorBox, false, false, 0);
|
||||
|
||||
// List of custom layouts (will be updated when monitor selection changes)
|
||||
let layoutsListContainer = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
|
||||
let layoutsList = createLayoutsList(0, monitorCombo, layoutsListContainer);
|
||||
layoutsListContainer.pack_start(layoutsList, true, true, 0);
|
||||
frame.pack_start(layoutsListContainer, true, true, 0);
|
||||
|
||||
// Help text
|
||||
let helpLabel = new Gtk.Label({
|
||||
@@ -62,7 +102,7 @@ function buildPrefsWidget() {
|
||||
return frame;
|
||||
}
|
||||
|
||||
function createLayoutsList() {
|
||||
function createLayoutsList(monitorIndex, monitorCombo, layoutsListContainer) {
|
||||
let scrolled = new Gtk.ScrolledWindow({
|
||||
shadow_type: Gtk.ShadowType.IN,
|
||||
min_content_height: 200
|
||||
@@ -73,8 +113,8 @@ function createLayoutsList() {
|
||||
});
|
||||
scrolled.add(listBox);
|
||||
|
||||
// Load custom layouts
|
||||
let layouts = loadCustomLayouts();
|
||||
// Load custom layouts for this monitor
|
||||
let layouts = loadCustomLayoutsForMonitor(monitorIndex);
|
||||
|
||||
if (Object.keys(layouts).length === 0) {
|
||||
let emptyRow = new Gtk.ListBoxRow({
|
||||
@@ -82,7 +122,7 @@ function createLayoutsList() {
|
||||
activatable: false
|
||||
});
|
||||
let emptyLabel = new Gtk.Label({
|
||||
label: '<i>No custom layouts yet</i>',
|
||||
label: '<i>No custom layouts yet for this monitor</i>',
|
||||
use_markup: true,
|
||||
margin: 20
|
||||
});
|
||||
@@ -90,15 +130,31 @@ function createLayoutsList() {
|
||||
listBox.add(emptyRow);
|
||||
} else {
|
||||
for (let layoutId in layouts) {
|
||||
let row = createLayoutRow(layoutId, layouts[layoutId], listBox);
|
||||
let row = createLayoutRow(layoutId, layouts[layoutId], listBox, monitorIndex);
|
||||
listBox.add(row);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect monitor combo box change event
|
||||
if (monitorCombo && layoutsListContainer) {
|
||||
monitorCombo.connect('changed', function() {
|
||||
let newMonitorIndex = monitorCombo.get_active();
|
||||
// Remove old scrolled window
|
||||
let children = layoutsListContainer.get_children();
|
||||
for (let child of children) {
|
||||
layoutsListContainer.remove(child);
|
||||
}
|
||||
// Add new one
|
||||
let newLayoutsList = createLayoutsList(newMonitorIndex, monitorCombo, layoutsListContainer);
|
||||
layoutsListContainer.pack_start(newLayoutsList, true, true, 0);
|
||||
layoutsListContainer.show_all();
|
||||
});
|
||||
}
|
||||
|
||||
return scrolled;
|
||||
}
|
||||
|
||||
function createLayoutRow(layoutId, layout, listBox) {
|
||||
function createLayoutRow(layoutId, layout, listBox, monitorIndex) {
|
||||
let row = new Gtk.ListBoxRow();
|
||||
let hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
@@ -135,7 +191,7 @@ function createLayoutRow(layoutId, layout, listBox) {
|
||||
});
|
||||
deleteButton.get_style_context().add_class('destructive-action');
|
||||
deleteButton.connect('clicked', function() {
|
||||
deleteLayout(layoutId, row, listBox);
|
||||
deleteLayout(layoutId, row, listBox, monitorIndex);
|
||||
});
|
||||
hbox.pack_end(deleteButton, false, false, 0);
|
||||
|
||||
@@ -159,16 +215,43 @@ function loadCustomLayouts() {
|
||||
if (file.query_exists(null)) {
|
||||
let [success, contents] = file.load_contents(null);
|
||||
if (success) {
|
||||
return JSON.parse(contents.toString());
|
||||
let data = JSON.parse(contents.toString());
|
||||
|
||||
// Check if v2 format
|
||||
if (data.version === 2 && data.perMonitorLayouts) {
|
||||
return data;
|
||||
} else {
|
||||
// V1 format - migrate to v2
|
||||
return {
|
||||
version: 2,
|
||||
perMonitorLayouts: {
|
||||
'0': {
|
||||
currentLayout: 'grid-2x2',
|
||||
customLayouts: data
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
log('Error loading custom layouts: ' + e);
|
||||
}
|
||||
return {
|
||||
version: 2,
|
||||
perMonitorLayouts: {}
|
||||
};
|
||||
}
|
||||
|
||||
function loadCustomLayoutsForMonitor(monitorIndex) {
|
||||
let allData = loadCustomLayouts();
|
||||
if (allData.perMonitorLayouts && allData.perMonitorLayouts[monitorIndex]) {
|
||||
return allData.perMonitorLayouts[monitorIndex].customLayouts || {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveCustomLayouts(layouts) {
|
||||
function saveCustomLayouts(allData) {
|
||||
try {
|
||||
// Create directory if it doesn't exist
|
||||
let dir = Gio.File.new_for_path(STORAGE_DIR);
|
||||
@@ -177,7 +260,7 @@ function saveCustomLayouts(layouts) {
|
||||
}
|
||||
|
||||
let file = Gio.File.new_for_path(LAYOUTS_FILE);
|
||||
let contents = JSON.stringify(layouts, null, 2);
|
||||
let contents = JSON.stringify(allData, null, 2);
|
||||
file.replace_contents(contents, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
||||
return true;
|
||||
} catch(e) {
|
||||
@@ -186,7 +269,7 @@ function saveCustomLayouts(layouts) {
|
||||
}
|
||||
}
|
||||
|
||||
function deleteLayout(layoutId, row, listBox) {
|
||||
function deleteLayout(layoutId, row, listBox, monitorIndex) {
|
||||
let dialog = new Gtk.MessageDialog({
|
||||
message_type: Gtk.MessageType.QUESTION,
|
||||
buttons: Gtk.ButtonsType.YES_NO,
|
||||
@@ -196,10 +279,16 @@ function deleteLayout(layoutId, row, listBox) {
|
||||
|
||||
dialog.connect('response', function(dialog, response) {
|
||||
if (response === Gtk.ResponseType.YES) {
|
||||
let layouts = loadCustomLayouts();
|
||||
delete layouts[layoutId];
|
||||
let allData = loadCustomLayouts();
|
||||
|
||||
if (saveCustomLayouts(layouts)) {
|
||||
// Delete from the specific monitor's layouts
|
||||
if (allData.perMonitorLayouts &&
|
||||
allData.perMonitorLayouts[monitorIndex] &&
|
||||
allData.perMonitorLayouts[monitorIndex].customLayouts) {
|
||||
delete allData.perMonitorLayouts[monitorIndex].customLayouts[layoutId];
|
||||
}
|
||||
|
||||
if (saveCustomLayouts(allData)) {
|
||||
listBox.remove(row);
|
||||
|
||||
// If no more layouts, show empty message
|
||||
@@ -210,7 +299,7 @@ function deleteLayout(layoutId, row, listBox) {
|
||||
activatable: false
|
||||
});
|
||||
let emptyLabel = new Gtk.Label({
|
||||
label: '<i>No custom layouts yet</i>',
|
||||
label: '<i>No custom layouts yet for this monitor</i>',
|
||||
use_markup: true,
|
||||
margin: 20
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user