How to grab and keep focus on a widget?

Hii
I have an Adw.Bin which contains a scrolled window with a drawing area. I want to grab focus on the bin and process key inputs like keyboard up, down, left, right, space bar. However, event after adding Gtk.EventControllerKey to the bin, it doesn’t receive the key presses.

The following is a minimal example. To keep the code simple, the bin contains a label (with a CSS frame to know its boundary). The bin has an event controller but it doesn’t work.

import gi

gi.require_versions({"Adw": "1"})
from gi.repository import Adw, Gtk


class MyWidget(Adw.Bin):
    __gtype_name__ = "MyWidget"

    def __init__(self):
        super().__init__(can_focus=True, vexpand=True)
        label = Gtk.Label(label="I Need Focus")
        self.set_child(label)
        ev = Gtk.EventControllerKey()
        ev.connect("key-pressed", print)
        self.add_controller(ev)
        self.add_css_class("frame")


def on_activate(application):
    win = Adw.ApplicationWindow(application=application)
    box = Gtk.Box(
        orientation=Gtk.Orientation.VERTICAL,
        spacing=10,
        halign=Gtk.Align.CENTER,
    )
    button = Gtk.Button(label="Don't Focus Me")
    wid = MyWidget()
    box.append(button)
    box.append(wid)
    win.set_content(box)
    win.set_default_size(200, 300)
    win.present()


app = Adw.Application(application_id="com.example.app")
app.connect("activate", on_activate)
app.run([])

I think I can add the event controller to the main window and manually process the key inputs, but I think this is a bad approach, since other widgets inside the window like entries, buttons may not be accessible via keyboard. The issue is, I need to receive inputs of the arrow keys and space bar, which are also part of keyboard navigation.

So how do I grab those key presses when my widget is focused without disturbing other elements?

widget_in_question.grab_focus() should do the trick. I did this recently, should work :slight_smile:

1 Like

Can you share a minimal example please?
I tried this and it doesn’t work :disappointed:

...
  self.add_css_class("frame")
  self.connect("show", lambda *_: self.grab_focus())


def on_activate(application):
    win = Adw.ApplicationWindow(application=application)
...

Do you do this with Gtk 4?
If so, widgets start their life cycle already being shown in Gtk4, so it is possible this never gets triggered. In my code I grab focus on mouse click, thus trough another event controller.

1 Like

Yep I’m using GTK 4.

I think, I should do something more to make the Adw.Bin receive focus.
For example, using an entry instead of bin works perfectly well.

import gi

gi.require_versions({"Adw": "1"})
from gi.repository import Adw, Gtk


class MyWidget(Adw.Bin):
    __gtype_name__ = "MyWidget"

    def __init__(self):
        super().__init__(can_focus=True, vexpand=True)
        label = Gtk.Label(label="I Need Focus")
        self.set_child(label)
        ev = Gtk.EventControllerKey()
        ev.connect("key-pressed", print)
        self.add_controller(ev)
        self.add_css_class("frame")


def on_activate(application):
    win = Adw.ApplicationWindow(application=application)
    box = Gtk.Box(
        orientation=Gtk.Orientation.VERTICAL,
        spacing=10,
        halign=Gtk.Align.CENTER,
    )
    button = Gtk.Button(label="Don't Focus Me")
    wid = MyWidget()
    ent = Gtk.Entry()
    button.connect("clicked", lambda _: ent.grab_focus())
    box.append(button)
    box.append(wid)
    box.append(ent)
    win.set_content(box)
    win.set_default_size(200, 300)
    win.present()


app = Adw.Application(application_id="com.example.app")
app.connect("activate", on_activate)
app.run([])

So what should I do to make the bin receive focus? :disappointed:

Heh, wrong account post :smiley:

Your last code works, because ‘clicked’ event is actually happening and that’s why grab_focus() works.

Question - how do you see life cycle of this widget? Should focus be grabbed after certain UI interactions or every time windows come in focus? This would allow to place grab_focus() where would actually happen and matter. For example, it might be a click on widget, or opening window.

Edit: check this property for Gtk.Window:is-active Gtk.Window:is-active
I would listen for this property via window.connect(“notify::is-active”, callback_func) and then do grab_focus() within callback_func.

1 Like

But if I replace the word ent with wid in signal handler, it doesn’t work.

button.connect("clicked", lambda _: wid.grab_focus())

In my real app, the widget is a page in a stack. So whenever the page is visible the widget must be focused. How to do it?

But if I replace the word ent with wid in signal handler, it doesn’t work.

In this case you needed to connect to wid.connect(“notify::is-active”, lambda _: ent.grab_focus()) - so Entry ent object would grab focus when Window wid object would get “is-active” property changed.

In my real app, the widget is a page in a stack. So whenever the page is visible the widget must be focused. How to do it?

See this Emmanuele comment - you need to connect stack signal “notify::visible-child-name”, and then in callback function check if name is page you want it to show, if yes, then grab focus for Entry object within that page. Something like this:

stack.connect("notify::visible-child-name", callback_func)
 def callback_func():
    if stack.get_property("visible-child-name") == page_which_contains_entry:
        ent.grab_focus()
1 Like

Sorry if I’m not clear, but what I’m saying is, the entry is able to grab the focus but the custom widget is not able to. :sweat_smile:

Or in other words, the Adw.Bin is not able to receive focus. That’s what I’m stuck at…

You must set the focusable property. It should be super().__init__(focusable=True, vexpand=True)

2 Likes

Wow thanks! That was the missing key!

Now to second question, is it possible to lock the focus to the widget? For example in the sample, if I press UP arrow, it goes to the button or DOWN arrow goes to the entry. I don’t want those keys to change the focus, is it possible?

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.