//! Keyboard Shortcuts System //! //! Manages application-wide keyboard shortcuts with modifier support. //! Provides human-readable shortcut text (e.g., "Ctrl+S"). const std = @import("std"); const Input = @import("input.zig"); /// Maximum shortcuts that can be registered const MAX_SHORTCUTS = 128; /// Key modifiers pub const Modifiers = packed struct { ctrl: bool = false, shift: bool = false, alt: bool = false, super: bool = false, // Windows/Command key pub fn none() Modifiers { return .{}; } pub fn ctrl_only() Modifiers { return .{ .ctrl = true }; } pub fn ctrl_shift() Modifiers { return .{ .ctrl = true, .shift = true }; } pub fn alt_only() Modifiers { return .{ .alt = true }; } pub fn eql(self: Modifiers, other: Modifiers) bool { return self.ctrl == other.ctrl and self.shift == other.shift and self.alt == other.alt and self.super == other.super; } pub fn fromInput(input: *const Input.InputState) Modifiers { return .{ .ctrl = input.keyDown(.left_ctrl) or input.keyDown(.right_ctrl), .shift = input.keyDown(.left_shift) or input.keyDown(.right_shift), .alt = input.keyDown(.left_alt) or input.keyDown(.right_alt), .super = input.modifiers.super, // Use tracked modifier state }; } }; /// A keyboard shortcut definition pub const Shortcut = struct { /// The key code key: Input.Key, /// Required modifiers modifiers: Modifiers, /// Action identifier action: []const u8, /// Human-readable description description: []const u8 = "", /// Is this shortcut enabled enabled: bool = true, /// Create a shortcut with Ctrl modifier pub fn ctrl(key: Input.Key, action: []const u8) Shortcut { return .{ .key = key, .modifiers = Modifiers.ctrl_only(), .action = action, }; } /// Create a shortcut with Ctrl+Shift modifiers pub fn ctrlShift(key: Input.Key, action: []const u8) Shortcut { return .{ .key = key, .modifiers = Modifiers.ctrl_shift(), .action = action, }; } /// Create a shortcut with Alt modifier pub fn alt(key: Input.Key, action: []const u8) Shortcut { return .{ .key = key, .modifiers = Modifiers.alt_only(), .action = action, }; } /// Create a shortcut with no modifiers pub fn key_only(key: Input.Key, action: []const u8) Shortcut { return .{ .key = key, .modifiers = Modifiers.none(), .action = action, }; } /// Check if this shortcut matches the given key and modifiers pub fn matches(self: Shortcut, key: Input.Key, mods: Modifiers) bool { return self.enabled and self.key == key and self.modifiers.eql(mods); } }; /// Standard shortcut IDs for quick checking without registration pub const StandardShortcut = enum { select_all, copy, paste, cut, undo, redo, save, new, open, close, find, quit, help, cancel, }; /// Check if a standard shortcut is active (convenience function) /// Works without ShortcutManager - checks input directly pub fn isStandardActive(input: *const Input.InputState, shortcut: StandardShortcut) bool { const mods = Modifiers.fromInput(input); return switch (shortcut) { .select_all => input.keyPressed(.a) and mods.eql(.{ .ctrl = true }), .copy => input.keyPressed(.c) and mods.eql(.{ .ctrl = true }), .paste => input.keyPressed(.v) and mods.eql(.{ .ctrl = true }), .cut => input.keyPressed(.x) and mods.eql(.{ .ctrl = true }), .undo => input.keyPressed(.z) and mods.eql(.{ .ctrl = true }), .redo => (input.keyPressed(.z) and mods.eql(.{ .ctrl = true, .shift = true })) or (input.keyPressed(.y) and mods.eql(.{ .ctrl = true })), .save => input.keyPressed(.s) and mods.eql(.{ .ctrl = true }), .new => input.keyPressed(.n) and mods.eql(.{ .ctrl = true }), .open => input.keyPressed(.o) and mods.eql(.{ .ctrl = true }), .close => input.keyPressed(.w) and mods.eql(.{ .ctrl = true }), .find => input.keyPressed(.f) and mods.eql(.{ .ctrl = true }), .quit => input.keyPressed(.q) and mods.eql(.{ .ctrl = true }), .help => input.keyPressed(.f1) and mods.eql(.{}), .cancel => input.keyPressed(.escape) and mods.eql(.{}), }; } /// Shortcut manager for registering and checking shortcuts pub const ShortcutManager = struct { shortcuts: [MAX_SHORTCUTS]Shortcut = undefined, count: usize = 0, const Self = @This(); /// Initialize a new shortcut manager pub fn init() Self { return .{}; } /// Register a shortcut pub fn register(self: *Self, shortcut: Shortcut) void { if (self.count >= MAX_SHORTCUTS) return; self.shortcuts[self.count] = shortcut; self.count += 1; } /// Register common shortcuts (Ctrl+C, Ctrl+V, etc.) pub fn registerCommon(self: *Self) void { self.register(Shortcut.ctrl(.c, "copy")); self.register(Shortcut.ctrl(.v, "paste")); self.register(Shortcut.ctrl(.x, "cut")); self.register(Shortcut.ctrl(.z, "undo")); self.register(Shortcut.ctrlShift(.z, "redo")); self.register(Shortcut.ctrl(.y, "redo")); self.register(Shortcut.ctrl(.s, "save")); self.register(Shortcut.ctrl(.a, "select_all")); self.register(Shortcut.ctrl(.n, "new")); self.register(Shortcut.ctrl(.o, "open")); self.register(Shortcut.ctrl(.w, "close")); self.register(Shortcut.ctrl(.q, "quit")); self.register(Shortcut.ctrl(.f, "find")); self.register(Shortcut.key_only(.f1, "help")); self.register(Shortcut.key_only(.escape, "cancel")); } /// Unregister a shortcut by action pub fn unregister(self: *Self, action: []const u8) void { var i: usize = 0; while (i < self.count) { if (std.mem.eql(u8, self.shortcuts[i].action, action)) { // Shift remaining shortcuts down var j = i; while (j < self.count - 1) : (j += 1) { self.shortcuts[j] = self.shortcuts[j + 1]; } self.count -= 1; } else { i += 1; } } } /// Enable/disable a shortcut by action pub fn setEnabled(self: *Self, action: []const u8, enabled: bool) void { for (self.shortcuts[0..self.count]) |*shortcut| { if (std.mem.eql(u8, shortcut.action, action)) { shortcut.enabled = enabled; } } } /// Check if any shortcut was triggered /// Returns the action string if a shortcut matched, null otherwise pub fn check(self: *Self, input: *const Input.InputState) ?[]const u8 { const mods = Modifiers.fromInput(input); for (self.shortcuts[0..self.count]) |shortcut| { if (input.keyPressed(shortcut.key) and shortcut.matches(shortcut.key, mods)) { return shortcut.action; } } return null; } /// Get shortcut by action pub fn getByAction(self: Self, action: []const u8) ?Shortcut { for (self.shortcuts[0..self.count]) |shortcut| { if (std.mem.eql(u8, shortcut.action, action)) { return shortcut; } } return null; } /// Get human-readable text for a shortcut (e.g., "Ctrl+S") pub fn getShortcutText(self: Self, buf: []u8, action: []const u8) []const u8 { if (self.getByAction(action)) |shortcut| { return formatShortcut(buf, shortcut); } return ""; } /// Clear all shortcuts pub fn clear(self: *Self) void { self.count = 0; } }; /// Format a shortcut as human-readable text pub fn formatShortcut(buf: []u8, shortcut: Shortcut) []const u8 { var stream = std.io.fixedBufferStream(buf); const writer = stream.writer(); if (shortcut.modifiers.ctrl) { writer.writeAll("Ctrl+") catch return ""; } if (shortcut.modifiers.alt) { writer.writeAll("Alt+") catch return ""; } if (shortcut.modifiers.shift) { writer.writeAll("Shift+") catch return ""; } if (shortcut.modifiers.super) { writer.writeAll("Super+") catch return ""; } const key_name = keyName(shortcut.key); writer.writeAll(key_name) catch return ""; return buf[0..stream.pos]; } /// Get human-readable name for a key pub fn keyName(key: Input.Key) []const u8 { return switch (key) { .a => "A", .b => "B", .c => "C", .d => "D", .e => "E", .f => "F", .g => "G", .h => "H", .i => "I", .j => "J", .k => "K", .l => "L", .m => "M", .n => "N", .o => "O", .p => "P", .q => "Q", .r => "R", .s => "S", .t => "T", .u => "U", .v => "V", .w => "W", .x => "X", .y => "Y", .z => "Z", .@"0" => "0", .@"1" => "1", .@"2" => "2", .@"3" => "3", .@"4" => "4", .@"5" => "5", .@"6" => "6", .@"7" => "7", .@"8" => "8", .@"9" => "9", .f1 => "F1", .f2 => "F2", .f3 => "F3", .f4 => "F4", .f5 => "F5", .f6 => "F6", .f7 => "F7", .f8 => "F8", .f9 => "F9", .f10 => "F10", .f11 => "F11", .f12 => "F12", .escape => "Esc", .enter => "Enter", .tab => "Tab", .backspace => "Backspace", .insert => "Insert", .delete => "Delete", .home => "Home", .end => "End", .page_up => "PageUp", .page_down => "PageDown", .up => "Up", .down => "Down", .left => "Left", .right => "Right", .space => "Space", else => "?", }; } // ============================================================================= // Tests // ============================================================================= test "Modifiers equality" { const m1 = Modifiers{ .ctrl = true }; const m2 = Modifiers{ .ctrl = true }; const m3 = Modifiers{ .shift = true }; try std.testing.expect(m1.eql(m2)); try std.testing.expect(!m1.eql(m3)); } test "Shortcut creation" { const shortcut = Shortcut.ctrl(.s, "save"); try std.testing.expectEqual(Input.Key.s, shortcut.key); try std.testing.expect(shortcut.modifiers.ctrl); try std.testing.expectEqualStrings("save", shortcut.action); } test "Shortcut matches" { const shortcut = Shortcut.ctrl(.s, "save"); try std.testing.expect(shortcut.matches(.s, .{ .ctrl = true })); try std.testing.expect(!shortcut.matches(.s, .{ .shift = true })); try std.testing.expect(!shortcut.matches(.a, .{ .ctrl = true })); } test "ShortcutManager register" { var manager = ShortcutManager.init(); manager.register(Shortcut.ctrl(.s, "save")); manager.register(Shortcut.ctrl(.z, "undo")); try std.testing.expectEqual(@as(usize, 2), manager.count); } test "ShortcutManager getByAction" { var manager = ShortcutManager.init(); manager.register(Shortcut.ctrl(.s, "save")); const shortcut = manager.getByAction("save"); try std.testing.expect(shortcut != null); try std.testing.expectEqual(Input.Key.s, shortcut.?.key); const missing = manager.getByAction("nonexistent"); try std.testing.expect(missing == null); } test "ShortcutManager unregister" { var manager = ShortcutManager.init(); manager.register(Shortcut.ctrl(.s, "save")); manager.register(Shortcut.ctrl(.z, "undo")); try std.testing.expectEqual(@as(usize, 2), manager.count); manager.unregister("save"); try std.testing.expectEqual(@as(usize, 1), manager.count); try std.testing.expect(manager.getByAction("save") == null); try std.testing.expect(manager.getByAction("undo") != null); } test "formatShortcut" { var buf: [32]u8 = undefined; const ctrl_s = Shortcut.ctrl(.s, "save"); const text1 = formatShortcut(&buf, ctrl_s); try std.testing.expectEqualStrings("Ctrl+S", text1); const ctrl_shift_z = Shortcut.ctrlShift(.z, "redo"); const text2 = formatShortcut(&buf, ctrl_shift_z); try std.testing.expectEqualStrings("Ctrl+Shift+Z", text2); } test "registerCommon" { var manager = ShortcutManager.init(); manager.registerCommon(); try std.testing.expect(manager.count >= 10); try std.testing.expect(manager.getByAction("copy") != null); try std.testing.expect(manager.getByAction("paste") != null); try std.testing.expect(manager.getByAction("undo") != null); } test "isStandardActive" { var input = Input.InputState.init(); // Simulate Ctrl being held input.keys_down[@intFromEnum(Input.Key.left_ctrl)] = true; // Simulate 'a' key press this frame input.keys_down[@intFromEnum(Input.Key.a)] = true; input.keys_down_prev[@intFromEnum(Input.Key.a)] = false; try std.testing.expect(isStandardActive(&input, .select_all)); try std.testing.expect(!isStandardActive(&input, .copy)); // 'c' not pressed }