//! "Open layout": one field per parameter, prefilled with its default. //! //! A layout with no parameters never gets here — the caller opens it straight //! away rather than showing an empty dialog with nothing to fill in. //! //! The values are handed back as borrowed slices pointing into the entries, //! which is safe because the callback runs while the dialog is still up and //! the view copies everything during substitution. const std = @import("std"); const gtk = @import("gtk"); const Layouts = @import("Layouts.zig"); const OpenLayoutDialog = @This(); pub const Callback = *const fn ( ctx: ?*anyopaque, layout: *Layouts.Layout, bindings: []const Layouts.Binding, ) void; alloc: std.mem.Allocator, window: *gtk.Window, layout: *Layouts.Layout, /// One entry per parameter, in the layout's own order. entries: []*gtk.Entry, on_open: Callback, ctx: ?*anyopaque, pub fn present( alloc: std.mem.Allocator, parent: *gtk.Window, layout: *Layouts.Layout, on_open: Callback, ctx: ?*anyopaque, ) !void { const self = try alloc.create(OpenLayoutDialog); errdefer alloc.destroy(self); const entries = try alloc.alloc(*gtk.Entry, layout.parameters.len); errdefer alloc.free(entries); self.* = .{ .alloc = alloc, .window = gtk.Window.new(), .layout = layout, .entries = entries, .on_open = on_open, .ctx = ctx, }; var title_buf: [128]u8 = undefined; const title = std.fmt.bufPrintZ(&title_buf, "Open “{s}”", .{layout.name}) catch "Open layout"; self.window.setTitle(title); self.window.setTransientFor(parent); self.window.setModal(1); self.window.setDefaultSize(480, -1); self.window.as(gtk.Widget).addCssClass("playpen-dialog"); const content = gtk.Box.new(.vertical, 12); content.as(gtk.Widget).addCssClass("playpen-dialog-content"); const fields = gtk.Grid.new(); fields.setRowSpacing(8); fields.setColumnSpacing(12); for (layout.parameters, 0..) |param, i| { const label = gtk.Label.new(null); // The description is what the author wrote for a human; the bare name // is the fallback so a parameter is never unlabelled. var label_buf: [128]u8 = undefined; const text = if (param.description.len > 0) param.description else param.name; label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter"); label.setXalign(0); label.as(gtk.Widget).addCssClass("playpen-dialog-label"); fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1); const entry = gtk.Entry.new(); entry.as(gtk.Widget).setHexpand(1); setEntryText(entry, param.default); var hint_buf: [128]u8 = undefined; entry.setPlaceholderText( std.fmt.bufPrintZ(&hint_buf, "{{{{{s}}}}}", .{param.name}) catch null, ); // Enter anywhere in the form opens the layout, so a one-parameter // layout is type-and-go. _ = gtk.Entry.signals.activate.connect( entry, *OpenLayoutDialog, &onEntryActivate, self, .{}, ); fields.attach(entry.as(gtk.Widget), 1, @intCast(i), 1, 1); entries[i] = entry; } content.append(fields.as(gtk.Widget)); const buttons = gtk.Box.new(.horizontal, 8); buttons.as(gtk.Widget).setHalign(.end); const cancel = gtk.Button.newWithLabel("Cancel"); _ = gtk.Button.signals.clicked.connect(cancel, *OpenLayoutDialog, &onCancel, self, .{}); buttons.append(cancel.as(gtk.Widget)); const open = gtk.Button.newWithLabel("Open"); open.as(gtk.Widget).addCssClass("suggested-action"); _ = gtk.Button.signals.clicked.connect(open, *OpenLayoutDialog, &onOpen, self, .{}); buttons.append(open.as(gtk.Widget)); content.append(buttons.as(gtk.Widget)); self.window.setChild(content.as(gtk.Widget)); _ = gtk.Widget.signals.destroy.connect( self.window, *OpenLayoutDialog, &onDestroy, self, .{}, ); self.window.present(); if (entries.len > 0) _ = entries[0].as(gtk.Widget).grabFocus(); } fn setEntryText(entry: *gtk.Entry, text: []const u8) void { var buf: [1024]u8 = undefined; const z = std.fmt.bufPrintZ(&buf, "{s}", .{text}) catch return; entry.as(gtk.Editable).setText(z); } fn submit(self: *OpenLayoutDialog) void { var storage: [16]Layouts.Binding = undefined; const count = @min(self.entries.len, storage.len); for (self.entries[0..count], 0..) |entry, i| { storage[i] = .{ .name = self.layout.parameters[i].name, .value = std.mem.span(entry.as(gtk.Editable).getText()), }; } // The callback builds the tab while this window is still alive, so the // borrowed entry text stays valid for the whole substitution pass. self.on_open(self.ctx, self.layout, storage[0..count]); self.window.destroy(); } fn onOpen(_: *gtk.Button, self: *OpenLayoutDialog) callconv(.c) void { self.submit(); } fn onEntryActivate(_: *gtk.Entry, self: *OpenLayoutDialog) callconv(.c) void { self.submit(); } fn onCancel(_: *gtk.Button, self: *OpenLayoutDialog) callconv(.c) void { self.window.destroy(); } fn onDestroy(_: *gtk.Window, self: *OpenLayoutDialog) callconv(.c) void { self.alloc.free(self.entries); self.alloc.destroy(self); }