171 lines
5.3 KiB
Zig
171 lines
5.3 KiB
Zig
//! "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);
|
|
}
|