Add comprehensive settings panel with layout management
New Features: - Created settings-schema.json with customizable options: * Zone appearance (border width, colors, opacity) * Show/hide zone numbers * Enable/disable Shift+Drag snapping * Enable/disable keyboard snapping (Super+Ctrl+1-9) * Notification on window snap - Created settings.js with custom UI: * View all saved custom layouts * Delete custom layouts with confirmation dialog * Export layouts to JSON files * Visual list with layout info (name, zone count, ID) * Empty state when no custom layouts exist Extension Integration: - Integrated Settings API into extension.js - Zone overlay now respects user-configured colors and opacity - Border width is customizable - Zone numbers can be toggled on/off - Shift-drag and keyboard snap can be disabled via settings - Optional notifications when windows snap to zones - Settings properly cleaned up on extension destroy UI/UX Improvements: - Professional settings panel accessible from System Settings → Extensions - Layout management without editing JSON files manually - Real-time application of visual settings - Destructive actions (delete) require confirmation - Export functionality for sharing layouts Fixes TODO item #12 - Settings Panel Fixes TODO item #13 - Update Metadata (already done) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
280
settings.js
Normal file
280
settings.js
Normal file
@@ -0,0 +1,280 @@
|
||||
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 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);
|
||||
|
||||
// List of custom layouts
|
||||
let layoutsList = createLayoutsList();
|
||||
frame.pack_start(layoutsList, 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() {
|
||||
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
|
||||
let layouts = loadCustomLayouts();
|
||||
|
||||
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</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);
|
||||
listBox.add(row);
|
||||
}
|
||||
}
|
||||
|
||||
return scrolled;
|
||||
}
|
||||
|
||||
function createLayoutRow(layoutId, layout, listBox) {
|
||||
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);
|
||||
});
|
||||
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) {
|
||||
return JSON.parse(contents.toString());
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
log('Error loading custom layouts: ' + e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function saveCustomLayouts(layouts) {
|
||||
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(layouts, 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) {
|
||||
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 layouts = loadCustomLayouts();
|
||||
delete layouts[layoutId];
|
||||
|
||||
if (saveCustomLayouts(layouts)) {
|
||||
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</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();
|
||||
}
|
||||
Reference in New Issue
Block a user