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: 'GridSnap Settings', 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: 'Custom Layouts', 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: 'Use Super+Shift+E to create new layouts\nUse Super+Shift+Z to cycle through layouts', use_markup: true, xalign: 0, margin_top: 10 }); frame.pack_start(helpLabel, false, false, 0); // Storage location info let storageLabel = new Gtk.Label({ label: 'Layouts stored in: ' + LAYOUTS_FILE + '', 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: 'No custom layouts yet for this monitor', 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: '' + GLib.markup_escape_text(layout.name, -1) + '', use_markup: true, xalign: 0 }); vbox.pack_start(nameLabel, false, false, 0); let infoLabel = new Gtk.Label({ label: '' + layout.zones.length + ' zones | ID: ' + layoutId + '', 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: 'No custom layouts yet for this monitor', 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(); }