Search code examples
gtkgtk3gdk

GTK: How do I grab keyboard input for a dialog/splash window, so that keyinput works out of window region?


I noticed that when my mouse is out of the dialog area, the keyboard input stops working.

This is detrimental since I want this small app to grab keyboard, so that I could handle it through keyboard without having to move my mouse.

I tried: windowSetKeepAbove, windowSetSkipPagerHint, windowSetSkipTaskbarHint, and windowPresentWithTime. I still could not focus in the window. None of these seem to work.

Also tried Seat.grab function, it gave me GDK_GRAB_NOT_VIEWABLE. But I am running this after calling showAll on the main window. Why is it not viewable?

I am so confused now. Any help would be appreciated.

EDIT: It is written in gi-gtk binding of haskell, but I don't think the language would be relevant - it is pretty much 1-1 binding to the gtk library itself. (E.g. windowSetTypeHint corresponds toGtk.Window.set_type_hint)

Here is the close-to-minimal reproducible example. (I guess things like windowSetPosition could have culled out, but it should not affect much. onWidgetKeyPressEvent is to hook into key press event)

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE LambdaCase #-}

module Main where

import Control.Monad
import Data.Foldable
import Data.Text qualified as T
import GI.Gdk qualified as Gdk
import GI.Gio.Objects qualified as Gio
import GI.Gtk qualified as Gtk
import System.Exit

main :: IO ()
main = do
  -- Does not care crashing here
  Just app <- Gtk.applicationNew (Just $ T.pack "test.program") []
  Gio.onApplicationActivate app (activating app)
  status <- Gio.applicationRun app Nothing
  when (status /= 0) $ exitWith (ExitFailure $ fromIntegral status)
  where
    activating :: Gtk.Application -> IO ()
    activating app = do
      window <- Gtk.applicationWindowNew app >>= Gtk.toWindow
      Gtk.windowSetTitle window (T.pack "Test Program")
      Gtk.windowSetDefaultSize window 560 140
      Gtk.windowSetTypeHint window Gdk.WindowTypeHintDialog
      Gtk.windowSetPosition window Gtk.WindowPositionCenterAlways
      Gtk.windowSetKeepAbove window True
      Gtk.windowSetSkipPagerHint window True
      Gtk.windowSetSkipTaskbarHint window True
      Gtk.onWidgetKeyPressEvent window $
        Gdk.getEventKeyKeyval >=> \case
          Gdk.KEY_Escape -> True <$ Gtk.windowClose window
          _ -> pure False

      Gtk.widgetShowAll window

      screen <- Gtk.windowGetScreen window
      gdkWins <- Gdk.screenGetToplevelWindows screen
      seat <- Gdk.screenGetDisplay screen >>= Gdk.displayGetDefaultSeat
      event <- Gtk.getCurrentEvent
      putStrLn "Finding window"
      filterM (fmap (Gdk.WindowStateAbove `elem`) . Gdk.windowGetState) gdkWins
        >>= traverse_
          ( \win -> do
              putStrLn "Window found"
              Gdk.windowShow win
              stat <- Gdk.seatGrab seat win [Gdk.SeatCapabilitiesAll] True (Nothing @Gdk.Cursor) event Nothing
              print stat
          )

      pure ()

I know, horrible hack, but I don't know other ways to get Gdk.Window. Searched through the gtk library, could not find the way to take Gdk.Window out of Gtk.Window.

Still, it turns out that this hack have found the gdk window.

Running with e.g. cabal run prints:

Finding window
Window found
GrabStatusNotViewable

So I got: GDK_GRAB_NOT_VIEWABLE somehow. It turns out that later on when e.g. focus event is fired, grab works normally. But I want to grab the mouse/keyboard earlier.


Solution

  • It turns out that the window needs to be mapped to grab the seat. I suspect that showAll does not immediately map the window, so calling seatGrab there leads to an error.

    Hence, the better way would be grabbing the focus at Widget's map-event. It conveniently carries the Gdk window field in its event struct, which I could use to grab the seat. In haskell, getEventAnyWindow gets the gdk window from the EventAny struct.

    A haskell snippet setting window to grab the focus:

    -- | Grab the screen on window map.
    windowGrabOnMap :: MonadIO m => Window -> m ()
    windowGrabOnMap window = do
      afterWidgetMapEvent window $
        Gdk.getEventAnyWindow >=> \case
          Nothing -> pure False
          Just win -> do
            event <- getCurrentEvent
            seat <- Gdk.windowGetDisplay win >>= Gdk.displayGetDefaultSeat
            Gdk.seatGrab seat win [Gdk.SeatCapabilitiesAll] True (Nothing @Gdk.Cursor) event Nothing
            pure False
      pure ()