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>
370 lines
12 KiB
JavaScript
370 lines
12 KiB
JavaScript
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
|
|
const STORAGE_DIR = GLib.get_user_data_dir() + '/gridsnap';
|
|
const LAYOUTS_FILE = STORAGE_DIR + '/layouts.json';
|
|
|
|
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,
|
|
margin: 20,
|
|
spacing: 10
|
|
});
|
|
|
|
// Add title
|
|
let title = new Gtk.Label({
|
|
label: '<b>GridSnap Settings</b>',
|
|
use_markup: true,
|
|
xalign: 0
|
|
});
|
|
frame.pack_start(title, false, false, 0);
|
|
|
|
// Add separator
|
|
frame.pack_start(new Gtk.Separator({orientation: Gtk.Orientation.HORIZONTAL}), false, false, 10);
|
|
|
|
// Custom Layouts Section
|
|
let layoutsLabel = new Gtk.Label({
|
|
label: '<b>Custom Layouts</b>',
|
|
use_markup: true,
|
|
xalign: 0
|
|
});
|
|
frame.pack_start(layoutsLabel, false, false, 5);
|
|
|
|
// 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({
|
|
label: '<i>Use Super+Shift+E to create new layouts\nUse Super+Shift+Z to cycle through layouts</i>',
|
|
use_markup: true,
|
|
xalign: 0,
|
|
margin_top: 10
|
|
});
|
|
frame.pack_start(helpLabel, false, false, 0);
|
|
|
|
// Storage location info
|
|
let storageLabel = new Gtk.Label({
|
|
label: '<small>Layouts stored in: ' + LAYOUTS_FILE + '</small>',
|
|
use_markup: true,
|
|
xalign: 0,
|
|
margin_top: 5
|
|
});
|
|
frame.pack_start(storageLabel, false, false, 0);
|
|
|
|
frame.show_all();
|
|
return frame;
|
|
}
|
|
|
|
function createLayoutsList(monitorIndex, monitorCombo, layoutsListContainer) {
|
|
let scrolled = new Gtk.ScrolledWindow({
|
|
shadow_type: Gtk.ShadowType.IN,
|
|
min_content_height: 200
|
|
});
|
|
|
|
let listBox = new Gtk.ListBox({
|
|
selection_mode: Gtk.SelectionMode.NONE
|
|
});
|
|
scrolled.add(listBox);
|
|
|
|
// Load custom layouts for this monitor
|
|
let layouts = loadCustomLayoutsForMonitor(monitorIndex);
|
|
|
|
if (Object.keys(layouts).length === 0) {
|
|
let emptyRow = new Gtk.ListBoxRow({
|
|
selectable: false,
|
|
activatable: false
|
|
});
|
|
let emptyLabel = new Gtk.Label({
|
|
label: '<i>No custom layouts yet for this monitor</i>',
|
|
use_markup: true,
|
|
margin: 20
|
|
});
|
|
emptyRow.add(emptyLabel);
|
|
listBox.add(emptyRow);
|
|
} else {
|
|
for (let layoutId in layouts) {
|
|
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, monitorIndex) {
|
|
let row = new Gtk.ListBoxRow();
|
|
let hbox = new Gtk.Box({
|
|
orientation: Gtk.Orientation.HORIZONTAL,
|
|
spacing: 10,
|
|
margin: 10
|
|
});
|
|
|
|
// Layout info
|
|
let vbox = new Gtk.Box({
|
|
orientation: Gtk.Orientation.VERTICAL,
|
|
spacing: 5
|
|
});
|
|
|
|
let nameLabel = new Gtk.Label({
|
|
label: '<b>' + GLib.markup_escape_text(layout.name, -1) + '</b>',
|
|
use_markup: true,
|
|
xalign: 0
|
|
});
|
|
vbox.pack_start(nameLabel, false, false, 0);
|
|
|
|
let infoLabel = new Gtk.Label({
|
|
label: '<small>' + layout.zones.length + ' zones | ID: ' + layoutId + '</small>',
|
|
use_markup: true,
|
|
xalign: 0
|
|
});
|
|
vbox.pack_start(infoLabel, false, false, 0);
|
|
|
|
hbox.pack_start(vbox, true, true, 0);
|
|
|
|
// Delete button
|
|
let deleteButton = new Gtk.Button({
|
|
label: 'Delete',
|
|
valign: Gtk.Align.CENTER
|
|
});
|
|
deleteButton.get_style_context().add_class('destructive-action');
|
|
deleteButton.connect('clicked', function() {
|
|
deleteLayout(layoutId, row, listBox, monitorIndex);
|
|
});
|
|
hbox.pack_end(deleteButton, false, false, 0);
|
|
|
|
// Export button
|
|
let exportButton = new Gtk.Button({
|
|
label: 'Export',
|
|
valign: Gtk.Align.CENTER
|
|
});
|
|
exportButton.connect('clicked', function() {
|
|
exportLayout(layoutId, layout);
|
|
});
|
|
hbox.pack_end(exportButton, false, false, 0);
|
|
|
|
row.add(hbox);
|
|
return row;
|
|
}
|
|
|
|
function loadCustomLayouts() {
|
|
try {
|
|
let file = Gio.File.new_for_path(LAYOUTS_FILE);
|
|
if (file.query_exists(null)) {
|
|
let [success, contents] = file.load_contents(null);
|
|
if (success) {
|
|
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(allData) {
|
|
try {
|
|
// Create directory if it doesn't exist
|
|
let dir = Gio.File.new_for_path(STORAGE_DIR);
|
|
if (!dir.query_exists(null)) {
|
|
dir.make_directory_with_parents(null);
|
|
}
|
|
|
|
let file = Gio.File.new_for_path(LAYOUTS_FILE);
|
|
let contents = JSON.stringify(allData, null, 2);
|
|
file.replace_contents(contents, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
return true;
|
|
} catch(e) {
|
|
log('Error saving custom layouts: ' + e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function deleteLayout(layoutId, row, listBox, monitorIndex) {
|
|
let dialog = new Gtk.MessageDialog({
|
|
message_type: Gtk.MessageType.QUESTION,
|
|
buttons: Gtk.ButtonsType.YES_NO,
|
|
text: 'Delete this layout?',
|
|
secondary_text: 'This action cannot be undone.'
|
|
});
|
|
|
|
dialog.connect('response', function(dialog, response) {
|
|
if (response === Gtk.ResponseType.YES) {
|
|
let allData = loadCustomLayouts();
|
|
|
|
// 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
|
|
let children = listBox.get_children();
|
|
if (children.length === 0) {
|
|
let emptyRow = new Gtk.ListBoxRow({
|
|
selectable: false,
|
|
activatable: false
|
|
});
|
|
let emptyLabel = new Gtk.Label({
|
|
label: '<i>No custom layouts yet for this monitor</i>',
|
|
use_markup: true,
|
|
margin: 20
|
|
});
|
|
emptyRow.add(emptyLabel);
|
|
listBox.add(emptyRow);
|
|
emptyRow.show_all();
|
|
}
|
|
}
|
|
}
|
|
dialog.destroy();
|
|
});
|
|
|
|
dialog.show();
|
|
}
|
|
|
|
function exportLayout(layoutId, layout) {
|
|
let dialog = new Gtk.FileChooserDialog({
|
|
title: 'Export Layout',
|
|
action: Gtk.FileChooserAction.SAVE
|
|
});
|
|
|
|
dialog.add_button('Cancel', Gtk.ResponseType.CANCEL);
|
|
dialog.add_button('Save', Gtk.ResponseType.ACCEPT);
|
|
dialog.set_do_overwrite_confirmation(true);
|
|
dialog.set_current_name(layoutId + '.json');
|
|
|
|
let filter = new Gtk.FileFilter();
|
|
filter.set_name('JSON files');
|
|
filter.add_pattern('*.json');
|
|
dialog.add_filter(filter);
|
|
|
|
dialog.connect('response', function(dialog, response) {
|
|
if (response === Gtk.ResponseType.ACCEPT) {
|
|
try {
|
|
let file = Gio.File.new_for_path(dialog.get_filename());
|
|
let exportData = {};
|
|
exportData[layoutId] = layout;
|
|
let contents = JSON.stringify(exportData, null, 2);
|
|
file.replace_contents(contents, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
|
|
let successDialog = new Gtk.MessageDialog({
|
|
message_type: Gtk.MessageType.INFO,
|
|
buttons: Gtk.ButtonsType.OK,
|
|
text: 'Layout exported successfully!'
|
|
});
|
|
successDialog.connect('response', function() {
|
|
successDialog.destroy();
|
|
});
|
|
successDialog.show();
|
|
} catch(e) {
|
|
let errorDialog = new Gtk.MessageDialog({
|
|
message_type: Gtk.MessageType.ERROR,
|
|
buttons: Gtk.ButtonsType.OK,
|
|
text: 'Error exporting layout',
|
|
secondary_text: e.toString()
|
|
});
|
|
errorDialog.connect('response', function() {
|
|
errorDialog.destroy();
|
|
});
|
|
errorDialog.show();
|
|
}
|
|
}
|
|
dialog.destroy();
|
|
});
|
|
|
|
dialog.show();
|
|
}
|