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

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.

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 ()

Related

Using keyboard and external numpad in AutoHotInterception

What I need is to map my keys from external numpad to keys on my keyboard. I've decided to use the AutoHotInterception program from evilC. The overall goal is to be able to use windows mouse keys with my keyboard. I have come to the point where program registers both inputs from my numpad and keyboard and I can type numbers with my keyboard but it doesn't really affect the windows mouse keys.
This is what I have so far:
#SingleInstance force
#Persistent
#include Lib\AutoHotInterception.ahk
AHI := new AutoHotInterception()
keyboardId := AHI.GetKeyboardId(0x04D9, 0x8008)
numPadId := AHI.GetKeyboardId(0x0C45, 0x7663)
AHI.SubscribeKeyboard(numPadId, true, Func("KeyEvent"))
AHI.SubscribeKeyboard(keyboardId, true, Func("KeyEvent"))
return
KeyEvent(code, state){
ToolTip % "Keyboard Key - Code: " code ", State: " state
if (state) & (code=30)
{
Send, {NumpadUp}
}
}
^Esc::
ExitApp
The main problem is that you need to send input at hardware driver level to trigger WMK.
Then I'd like to say that your questions seems quite weird. It seems to contradict itself?
You want to remap keys from the external keyboard, but then you also say that you just want to use the numpad with your main keyboard? So what is the external keyboard for actually?
In any case, Here's some advise and a revised script you can probably adapt for your own use. So firstly there's really no need to subscribe to the whole keyboard. Just subscribe to the keys you want.
And since I'm quite unclear on what it is you're actually trying to do, I'll just show an example of how you'd remap A, S and D to Numpad1, Numpad2 and Numpad3 at hardware driver level.
#include Lib\AutoHotInterception.ahk
AHI := new AutoHotInterception()
;kind of optional
;if you don't switch up stuff, you'll always have the same id
keyboardId := AHI.GetKeyboardId(..., ...)
;binding arguments to the function object to make use of the same function over and over
;so don't need to define a new function for each key
AHI.SubscribeKey(keyboardId, GetKeySC("a"), true, Func("KeyEvent").Bind(GetKeySC("numpad1")))
AHI.SubscribeKey(keyboardId, GetKeySC("s"), true, Func("KeyEvent").Bind(GetKeySC("numpad2")))
AHI.SubscribeKey(keyboardId, GetKeySC("d"), true, Func("KeyEvent").Bind(GetKeySC("numpad3")))
return
;the key argument will be the argument we bound to the function object above
;the AHI library takes care of passing in the state argument
KeyEvent(key, state)
{
global keyboardId
AHI.SendKeyEvent(keyboardId, key, state)
}
^Esc::ExitApp
Sending input is documented here here.
(Side question, any chance you're doing this for OSRS?)

wxPython window randomly freezes on RPi and acts weird afterwards

This Problem occurs on the RPi (3B+, Raspbian Buster) only. I run the program on my Mac without any problems.
Short description of my program:
After entering the mainloop, the program enters a second thread with another loop (call it requestloop) when the designated button is pressed. The requestloop can be left by pressing the button again. This requestloop requests a xml table via url every 10 seconds which is then parsed with ElementTree, sorted and displayed in a wx.grid.Grid. I use grid.ForceRefresh to make sure the grid is updated.
I hope the following snippet helps to understand the above:
def on_btnrun(self, event):
global run
if self.btnrun.Label == "Start":
run = True
thrupt = threading.Thread(target=self.thrupdate)
thrupt.start()
self.btnrun.SetLabel("Ende")
elif self.btnrun.Label == "Ende":
run = False
self.btnrun.SetLabel("Start")
def thrupdate(self):
while run is True:
reset()
self.grid.ClearGrid()
update(self.grid)
self.grid.ForceRefresh()
time.sleep(10)
Problem:
Now as mentioned in the title the whole wx Window freezes after passing the requestloop between roughly 5 and 20 times. This happens completely randomly, I could not find any regularities. The program keeps running though, for it still prints the output in the terminal every cycle (I added this function for testing).
When I now open another window (eg. menu dropwdown) which lays over the wx Window it will be copied onto the wx Window and stay there after I closed it.
Here are some Images to better understand what I mean (ignore all other widgets that I didn't mention, they are just nonfunctional placeholders).
Image of the wx Window before it freezes
Image of the wx Window after it freezes
Image of the wx Window after opening and closing the dropdown menu
Extra-Info: while building wxPython on the RPi I got some warnings and everytime I run the program I get the following one (it says the actual time instead of time):
(program.py:1666): Gtk-Critical **: time: gtk_distribute_natural_allocation: assertion ‚extra_space >= 0‘ failed
Question:
I have no idea why any of this happens. Is wxPython not stable on Raspbian? Or did the build partly fail? Or is the RPi not having enough rendering capacity?
Solved it by using wx.CallAfter in the details of the update() method.

gtk_window_is_active() not working as expected

I call gtk_window_is_active(wnd) and always receive 0, even when I know for sure that wnd is active and receiving keyboard input. What is the cause and where is the remedy for this?
In fact, I run gtk_window_list_toplevels() and iterate over the list - and gtk_window_is_active() returns 0 for each of them!
When you create a GtkWindow it is still in the 'unrealized' state. You have to call show() on it and let the main loop run, then the window gets realized. So if you call gtk_window_is_active after creating the windows, but before the main loop has chances to run, you will get false.
Thanks to Emmanuele Bassi, Gnome Foundation staff, I figured it out: the problem is that my focus-in-event handler returned 1 (TRUE), and thus prevented the default GTK behaviour. It turned out (something not obvious) that keeping track of the active window is part of that default behavior that i unknowingly overrode.
So, I changed focus-in-event handler of my windows to return FALSE (0), and ever since gtk_window_is_active() works like a clock.
I came to realize an unhelpful (to my task) detail: gtk_window_is_active() only works AFTER all focus-in-event handlers have completed working. Well, I have a mouse click handler that activates some other window, and then needs to check if a certain window is active (these things belong to different objects and different modules, yet are executed within one click hadler invocation). In my case gtk_window_is_active() is useless: it returs FALSE for the active window until after my click handler has finished and the focus-in-handlers (mine and the default) have finished, too.

VsCode appears to keep running wxPython app when it should have stopped

In my VsCode wxPython application under Windows, I bind the close event to my function as follows:
import wx
class MyFrame(wx.Frame):
def __init__(self, title):
wx.Frame.__init__(self, None, title = title, pos = (150, 0))
self.Bind(wx.EVT_CLOSE, self.OnClose)
# Other stuff ...
def OnClose(self, event):
print("closing")
self.Destroy()
app = wx.App()
top = MyFrame("My App")
top.Show()
app.MainLoop()
print("done")
Now I can see it's being called when I close the top level window (a wxFrame) as the closing and done messages appear. The window itself also disappears.
However VsCode thinks the application is still running since it has the debug controls still available:
and the console that ran it (Python debug console) does not come back with a prompt. It's not until I click on the stop button does the command prompt reappear in that console.
Interestingly, if I run the application outside of VsCode, it exits correctly, returning to the command prompt.
Try wx.Exit() in case you have something still open/running without realising it.
wx.Exit()
Exits application after calling wx.App.OnExit .
Should only be used in an emergency: normally the top-level frame should be deleted (after deleting all other frames) to terminate the application. See wx.CloseEvent and wx.App.

PyGTK Hotkey Toggle Pin/Unpin

I'm still learning PyGTK, and I'm trying to figure out how I can tell the window to pin/unpin (aka toggle always on top) each time I press the F1 key.
This is what I got so far.
#!/usr/bin/env python
import gtk
class app(gtk.Window):
def pinning(self, widget, event) :
if event.keyval == gtk.keysyms.F1 :
self.set_keep_above(True)
def __init__(self):
super(app, self).__init__()
self.set_position(gtk.WIN_POS_CENTER)
self.set_title("TestApp")
self.set_default_size(320, 200)
self.connect("destroy", gtk.main_quit)
self.connect("key-press-event", self.pinning)
self.show_all()
app()
gtk.main()
Unfortunately, there is no get_keep_above() method. You have to connect to the window's window-state-event signal; in the event parameter, read the new_window_state field to see if the sticky flag is set or not, then keep track of it yourself, for example in self.is_kept_above.
Then you can do
if self.is_kept_above:
self.set_keep_above(False)
else:
self.set_keep_above(True)
in your F1 handler.
In general, you can capture keypresses and then emit whatever signal you want, like I wrote in this answer: PYGTK redirect event to TreeView