diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index dbe96aa2d..e00304491 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1268,7 +1268,7 @@ fn gtkMouseDown( const gtk_mods = c.gdk_event_get_modifier_state(event); const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture))); - const mods = translateMods(gtk_mods); + const mods = gtk_key.translateMods(gtk_mods); // If we don't have focus, grab it. const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area)); @@ -1300,7 +1300,7 @@ fn gtkMouseUp( const gtk_mods = c.gdk_event_get_modifier_state(event); const button = translateMouseButton(c.gtk_gesture_single_get_current_button(@ptrCast(gesture))); - const mods = translateMods(gtk_mods); + const mods = gtk_key.translateMods(gtk_mods); const self = userdataSelf(ud.?); _ = self.core_surface.mouseButtonCallback(.release, button, mods) catch |err| { @@ -1342,7 +1342,7 @@ fn gtkMouseMotion( // Get our modifiers const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)); const gtk_mods = c.gdk_event_get_modifier_state(event); - const mods = translateMods(gtk_mods); + const mods = gtk_key.translateMods(gtk_mods); self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { log.err("error in cursor pos callback err={}", .{err}); @@ -1359,7 +1359,7 @@ fn gtkMouseLeave( // Get our modifiers const event = c.gtk_event_controller_get_current_event(@ptrCast(ec)); const gtk_mods = c.gdk_event_get_modifier_state(event); - const mods = translateMods(gtk_mods); + const mods = gtk_key.translateMods(gtk_mods); self.core_surface.cursorPosCallback(.{ .x = -1, .y = -1 }, mods) catch |err| { log.err("error in cursor pos callback err={}", .{err}); return; @@ -1395,7 +1395,14 @@ fn gtkKeyPressed( gtk_mods: c.GdkModifierType, ud: ?*anyopaque, ) callconv(.C) c.gboolean { - return if (keyEvent(.press, ec_key, keyval, keycode, gtk_mods, ud)) 1 else 0; + const self = userdataSelf(ud.?); + return if (self.keyEvent( + .press, + ec_key, + keyval, + keycode, + gtk_mods, + )) 1 else 0; } fn gtkKeyReleased( @@ -1405,7 +1412,14 @@ fn gtkKeyReleased( state: c.GdkModifierType, ud: ?*anyopaque, ) callconv(.C) c.gboolean { - return if (keyEvent(.release, ec_key, keyval, keycode, state, ud)) 1 else 0; + const self = userdataSelf(ud.?); + return if (self.keyEvent( + .release, + ec_key, + keyval, + keycode, + state, + )) 1 else 0; } /// Key press event. This is where we do ALL of our key handling, @@ -1432,64 +1446,26 @@ fn gtkKeyReleased( /// Note we ALSO have an IMContext attached directly to the widget /// which can emit preedit and commit callbacks. But, if we're not /// in a keypress, we let those automatically work. -fn keyEvent( +pub fn keyEvent( + self: *Surface, action: input.Action, ec_key: *c.GtkEventControllerKey, keyval: c.guint, keycode: c.guint, gtk_mods: c.GdkModifierType, - ud: ?*anyopaque, ) bool { - const self = userdataSelf(ud.?); const keyval_unicode = c.gdk_keyval_to_unicode(keyval); - const event = c.gtk_event_controller_get_current_event(@ptrCast(ec_key)); - const display = c.gtk_widget_get_display(@ptrCast(self.gl_area)); + const event = c.gtk_event_controller_get_current_event( + @ptrCast(ec_key), + ) orelse return false; // Get the unshifted unicode value of the keyval. This is used // by the Kitty keyboard protocol. - const keyval_unicode_unshifted: u21 = unshifted: { - // We need to get the currently active keyboard layout so we know - // what group to look at. - const layout = c.gdk_key_event_get_layout(@ptrCast(event)); - - // Get all the possible keyboard mappings for this keycode. A keycode - // is the physical key pressed. - var keys: [*]c.GdkKeymapKey = undefined; - var keyvals: [*]c.guint = undefined; - var n_keys: c_int = 0; - if (c.gdk_display_map_keycode( - display, - keycode, - @ptrCast(&keys), - @ptrCast(&keyvals), - &n_keys, - ) == 0) break :unshifted 0; - - defer c.g_free(keys); - defer c.g_free(keyvals); - - // debugging: - // log.debug("layout={}", .{layout}); - // for (0..@intCast(n_keys)) |i| { - // log.debug("keymap key={} codepoint={x}", .{ - // keys[i], - // c.gdk_keyval_to_unicode(keyvals[i]), - // }); - // } - - for (0..@intCast(n_keys)) |i| { - if (keys[i].group == layout and - keys[i].level == 0) - { - break :unshifted std.math.cast( - u21, - c.gdk_keyval_to_unicode(keyvals[i]), - ) orelse 0; - } - } - - break :unshifted 0; - }; + const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted( + @ptrCast(self.gl_area), + event, + keycode, + ); // We always reset our committed text when ending a keypress so that // future keypresses don't think we have a commit event. @@ -1549,44 +1525,20 @@ fn keyEvent( if (entry.native == keycode) break :keycode entry.key; } else .invalid; - const mods = mods: { - const device = c.gdk_event_get_device(event); - - var mods = if (self.app.x11_xkb) |xkb| - // Add any modifier state events from Xkb if we have them (X11 - // only). Null back from the Xkb call means there was no modifier - // event to read. This likely means that the key event did not - // result in a modifier change and we can safely rely on the GDK - // state. - xkb.modifier_state_from_notify(display) orelse translateMods(gtk_mods) - else - // On Wayland, we have to use the GDK device because the mods sent - // to this event do not have the modifier key applied if it was - // presssed (i.e. left control). - translateMods(c.gdk_device_get_modifier_state(device)); - - mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1; - - switch (physical_key) { - .left_shift => mods.sides.shift = .left, - .right_shift => mods.sides.shift = .right, - .left_control => mods.sides.ctrl = .left, - .right_control => mods.sides.ctrl = .right, - .left_alt => mods.sides.alt = .left, - .right_alt => mods.sides.alt = .right, - .left_super => mods.sides.super = .left, - .right_super => mods.sides.super = .right, - else => {}, - } - - break :mods mods; - }; + // Get our modifier for the event + const mods: input.Mods = gtk_key.eventMods( + @ptrCast(self.gl_area), + event, + physical_key, + gtk_mods, + if (self.app.x11_xkb) |*xkb| xkb else null, + ); // Get our consumed modifiers const consumed_mods: input.Mods = consumed: { const raw = c.gdk_key_event_get_consumed_modifiers(event); const masked = raw & c.GDK_MODIFIER_MASK; - break :consumed translateMods(masked); + break :consumed gtk_key.translateMods(masked); }; // If we're not in a dead key state, we want to translate our text @@ -1908,18 +1860,6 @@ fn translateMouseButton(button: c.guint) input.MouseButton { }; } -fn translateMods(state: c.GdkModifierType) input.Mods { - var mods: input.Mods = .{}; - if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true; - if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true; - if (state & c.GDK_ALT_MASK != 0) mods.alt = true; - if (state & c.GDK_SUPER_MASK != 0) mods.super = true; - - // Lock is dependent on the X settings but we just assume caps lock. - if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; - return mods; -} - pub fn present(self: *Surface) void { if (self.container.window()) |window| { if (self.container.tab()) |tab| { diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index ff8735ff9..efde78e61 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -21,6 +21,7 @@ const Surface = @import("Surface.zig"); const Tab = @import("Tab.zig"); const c = @import("c.zig").c; const adwaita = @import("adwaita.zig"); +const gtk_key = @import("key.zig"); const Notebook = @import("notebook.zig").Notebook; const log = std.log.scoped(.gtk); @@ -255,10 +256,18 @@ pub fn init(self: *Window, app: *App) !void { // If we are in fullscreen mode, new windows start fullscreen. if (app.config.fullscreen) c.gtk_window_fullscreen(self.window); + // We register a key event controller with the window so + // we can catch key events when our surface may not be + // focused (i.e. when the libadw tab overview is shown). + const ec_key_press = c.gtk_event_controller_key_new(); + errdefer c.g_object_unref(ec_key_press); + c.gtk_widget_add_controller(window, ec_key_press); + // All of our events _ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); // Our actions for the menu initActions(self); @@ -662,6 +671,40 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { alloc.destroy(self); } +fn gtkKeyPressed( + ec_key: *c.GtkEventControllerKey, + keyval: c.guint, + keycode: c.guint, + gtk_mods: c.GdkModifierType, + ud: ?*anyopaque, +) callconv(.C) c.gboolean { + const self = userdataSelf(ud.?); + + // We only process window-level events currently for the tab + // overview. This is primarily defensive programming because + // I'm not 100% certain how our logic below will interact with + // other parts of the application but I know for sure we must + // handle this during the tab overview. + // + // If someone can confidently show or explain that this is not + // necessary, please remove this check. + if (comptime adwaita.versionAtLeast(1, 4, 0)) { + if (self.tab_overview) |tab_overview_widget| { + const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget)); + if (c.adw_tab_overview_get_open(tab_overview) == 0) return 0; + } + } + + const surface = self.app.core_app.focusedSurface() orelse return 0; + return if (surface.rt_surface.keyEvent( + .press, + ec_key, + keyval, + keycode, + gtk_mods, + )) 1 else 0; +} + fn gtkActionAbout( _: *c.GSimpleAction, _: *c.GVariant, diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index da4f6ea2b..f8458bd3b 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -1,6 +1,7 @@ const std = @import("std"); const input = @import("../../input.zig"); const c = @import("c.zig").c; +const x11 = @import("x11.zig"); /// Returns a GTK accelerator string from a trigger. pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 { @@ -47,6 +48,102 @@ pub fn translateMods(state: c.GdkModifierType) input.Mods { return mods; } +// Get the unshifted unicode value of the keyval. This is used +// by the Kitty keyboard protocol. +pub fn keyvalUnicodeUnshifted( + widget: *c.GtkWidget, + event: *c.GdkEvent, + keycode: c.guint, +) u21 { + const display = c.gtk_widget_get_display(widget); + + // We need to get the currently active keyboard layout so we know + // what group to look at. + const layout = c.gdk_key_event_get_layout(@ptrCast(event)); + + // Get all the possible keyboard mappings for this keycode. A keycode + // is the physical key pressed. + var keys: [*]c.GdkKeymapKey = undefined; + var keyvals: [*]c.guint = undefined; + var n_keys: c_int = 0; + if (c.gdk_display_map_keycode( + display, + keycode, + @ptrCast(&keys), + @ptrCast(&keyvals), + &n_keys, + ) == 0) return 0; + + defer c.g_free(keys); + defer c.g_free(keyvals); + + // debugging: + // log.debug("layout={}", .{layout}); + // for (0..@intCast(n_keys)) |i| { + // log.debug("keymap key={} codepoint={x}", .{ + // keys[i], + // c.gdk_keyval_to_unicode(keyvals[i]), + // }); + // } + + for (0..@intCast(n_keys)) |i| { + if (keys[i].group == layout and + keys[i].level == 0) + { + return std.math.cast( + u21, + c.gdk_keyval_to_unicode(keyvals[i]), + ) orelse 0; + } + } + + return 0; +} + +/// Returns the mods to use a key event from a GTK event. +/// This requires a lot of context because the GdkEvent +/// doesn't contain enough on its own. +pub fn eventMods( + widget: *c.GtkWidget, + event: *c.GdkEvent, + physical_key: input.Key, + gtk_mods: c.GdkModifierType, + x11_xkb: ?*x11.Xkb, +) input.Mods { + const device = c.gdk_event_get_device(event); + const display = c.gtk_widget_get_display(widget); + + var mods = if (x11_xkb) |xkb| + // Add any modifier state events from Xkb if we have them (X11 + // only). Null back from the Xkb call means there was no modifier + // event to read. This likely means that the key event did not + // result in a modifier change and we can safely rely on the GDK + // state. + xkb.modifier_state_from_notify(display) orelse + translateMods(gtk_mods) + else + // On Wayland, we have to use the GDK device because the mods sent + // to this event do not have the modifier key applied if it was + // presssed (i.e. left control). + translateMods(c.gdk_device_get_modifier_state(device)); + + mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1; + + switch (physical_key) { + .left_shift => mods.sides.shift = .left, + .right_shift => mods.sides.shift = .right, + .left_control => mods.sides.ctrl = .left, + .right_control => mods.sides.ctrl = .right, + .left_alt => mods.sides.alt = .left, + .right_alt => mods.sides.alt = .right, + .left_super => mods.sides.super = .left, + .right_super => mods.sides.super = .right, + else => {}, + } + + return mods; +} + /// Returns an input key from a keyval or null if we don't have a mapping. pub fn keyFromKeyval(keyval: c.guint) ?input.Key { for (keymap) |entry| {