const std = @import("std"); const assert = @import("../../../quirks.zig").inlineAssert; const testing = std.testing; const gio = @import("gio"); const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); const gValueHolds = @import("../ext.zig").gValueHolds; /// Check that an action name is valid. /// /// Reimplementation of `g_action_name_is_valid()` so that it can be /// used at comptime. /// /// See: /// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html fn gActionNameIsValid(name: [:0]const u8) bool { if (name.len == 0) return false; for (name) |c| switch (c) { '-' => continue, '.' => continue, '0'...'9' => continue, 'a'...'z' => continue, 'A'...'Z' => continue, else => return false, }; return true; } test "gActionNameIsValid" { try testing.expect(gActionNameIsValid("ring-bell")); try testing.expect(!gActionNameIsValid("ring_bell")); } /// Function to create a structure for describing an action. pub fn Action(comptime T: type) type { return struct { const Self = @This(); pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType, state: ?*glib.Variant = null, /// Function to initialize a new action so that we can comptime check /// the name. pub fn init( comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType, ) Self { comptime assert(gActionNameIsValid(name)); return .{ .name = name, .callback = callback, .parameter_type = parameter_type, }; } /// Function to initialize a new stateful action so that we can comptime /// check the name. pub fn initStateful( comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType, state: *glib.Variant, ) Self { comptime assert(gActionNameIsValid(name)); return .{ .name = name, .callback = callback, .parameter_type = parameter_type, .state = state, }; } }; } /// Add actions to a widget that implements gio.ActionMap. pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void { addToMap(T, self, self.as(gio.ActionMap), actions); } /// Add actions to the given map. pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void { for (actions) |entry| { assert(gActionNameIsValid(entry.name)); const action = action: { if (entry.state) |state| { break :action gio.SimpleAction.newStateful( entry.name, entry.parameter_type, state, ); } break :action gio.SimpleAction.new( entry.name, entry.parameter_type, ); }; defer action.unref(); _ = gio.SimpleAction.signals.activate.connect( action, *T, entry.callback, self, .{}, ); map.addAction(action.as(gio.Action)); } } /// Add actions to a widget that doesn't implement ActionGroup directly. pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup { comptime assert(gActionNameIsValid(name)); // Collect our actions into a group since we're just a plain widget that // doesn't implement ActionGroup directly. const group = gio.SimpleActionGroup.new(); errdefer group.unref(); addToMap(T, self, group.as(gio.ActionMap), actions); self.as(gtk.Widget).insertActionGroup( name, group.as(gio.ActionGroup), ); return group; } test "adding actions to an object" { // This test requires a connection to an active display environment. if (gtk.initCheck() == 0) return error.SkipZigTest; _ = glib.MainContext.acquire(null); defer glib.MainContext.release(null); const callbacks = struct { fn callback(_: *gio.SimpleAction, variant_: ?*glib.Variant, self: *gtk.Box) callconv(.c) void { const i32_variant_type = glib.ext.VariantType.newFor(i32); defer i32_variant_type.free(); const variant = variant_ orelse return; assert(variant.isOfType(i32_variant_type) != 0); var value = std.mem.zeroes(gobject.Value); _ = value.init(gobject.ext.types.int); defer value.unset(); value.setInt(variant.getInt32()); self.as(gobject.Object).setProperty("spacing", &value); } }; const box = gtk.Box.new(.vertical, 0); _ = box.as(gobject.Object).refSink(); defer box.unref(); { const i32_variant_type = glib.ext.VariantType.newFor(i32); defer i32_variant_type.free(); const actions = [_]Action(gtk.Box){ .init("test", callbacks.callback, i32_variant_type), }; _ = addAsGroup(gtk.Box, box, "test", &actions); } const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31)); const parameter = glib.Variant.newInt32(expected); try testing.expect(box.as(gtk.Widget).activateActionVariant("test.test", parameter) != 0); _ = glib.MainContext.iteration(null, @intFromBool(true)); var value = std.mem.zeroes(gobject.Value); _ = value.init(gobject.ext.types.int); defer value.unset(); box.as(gobject.Object).getProperty("spacing", &value); try testing.expect(gValueHolds(&value, gobject.ext.types.int)); const actual = value.getInt(); try testing.expectEqual(expected, actual); while (glib.MainContext.iteration(null, 0) != 0) {} }