PyGTK Entry widget in TreeViewColumn header
Asked Answered
P

3

8

How can I make a gtk.Entry widget focusable or editable within a gtk.TreeViewColumn header/title? I've tried this:

# Create tree-view.
treeview = gtk.TreeView()

#...

# Create column.
renderer = gtk.CellRendererText()
column = gtk.TreeViewColumn(None, renderer, text=0)

# Set column header.
header = gtk.VBox()

title = gtk.Label("Column")
header.pack_start(title)

filter = gtk.Entry()
#...
header.pack_start(filter)

header.show_all()
column.set_widget(header)

# Add column
treeview.append_column(column)

But the Entry widget in the column header is not editable and will not focus. I've tried setting 'clickable' to both True and False. I'm using pygtk 2.21.0-0ubuntu1 and libgtk 2.22.0-0ubuntu1 on Ubuntu 10.04. Any help would be greatly appreciated.

EDIT:

The issue stems from how a GtkTreeViewColumn header is displayed. The header widget is placed inside a GtkAlignment whose parent is a GtkHBox whose parent is a GtkButton whose parent is finally the GtkTreeView. The GtkButton is intercepting and preventing my GtkEntry from being focused and receiving mouse input.

Pique answered 7/3, 2011 at 18:41 Comment(0)
P
13

In order to make a GtkEntry focusable within a GtkTreeView header I had to:

1) Find the header GtkButton.

def find_closest_ancestor(widget, ancestor_class):
    if not isinstance(widget, gtk.Widget):
        raise TypeError("%r is not a gtk.Widget" % widget)
    ancestor = widget.get_parent()
    while ancestor is not None:
        if isinstance(ancestor, ancestor_class):
            break;
        ancestor = ancestor.get_parent() if hasattr(ancestor, 'get_parent') and callable(ancestor.get_parent) else None
    return ancestor

2) Propagate the button-press-event signal from the header GtkButton to the GtkEntry.

def propagate_button_press_event(parent, event, *data):
    parent_alloc = parent.get_allocation()
    x = parent_alloc.x + int(event.x)
    y = parent_alloc.y + int(event.y)
    children = parent.get_children()
    print "Propagating event:%r" % event
    print "- from parent:%r" % parent
    while children:
        for child in children:
            child_alloc = child.get_allocation()
            if child_alloc.x <= x <= child_alloc.x + child_alloc.width and child_alloc.y <= y <= child_alloc.y + child_alloc.height:
                print "- to child:%r" % child
                if child.get_property('can-focus'):
                    event.send_event = True
                    child.grab_focus()
                    child.emit('button-press-event', event, *data)
                    return True
                else:
                    children = child.get_children() if hasattr(child, 'get_children') and callable(child.get_children) else None
                    break;
        else:
            children = None
    return False

3) Propagate the focus (i.e., focus-in-event signal) from the header GtkButton to the GtkEntry.

def propagate_focus_in_event(parent, event, *data):
    print 'focus-in', parent, event
    child = parent.get_child()
    if child.get_property('can-focus'):
        child.grab_focus()
    else:
        if not child.child_focus(gtk.DIR_TAB_FORWARD):
            parent.get_toplevel().child_focus(gtk.DIR_TAB_FORWARD)
    return True

Example:

# Fix style glitches
_gtk_styles = """
    # Use the default GtkEntry style for GtkEntry widgets in treeview headers.
    widget "*.treeview-header-entry" style "entry" 
"""
gtk.rc_parse_string(_gtk_styles)

# Columns
_columns = [
    (0, "Title"),
    (1, "Description")
    # etc.
]

# Create tree-view.
items_view = gtk.TreeView(self.items_store)
items_view.show()

# Setup treeview columns.
renderer = gtk.CellRendererText()
for column in _columns:
    column_index, column_title, column_filter = column
    column_view = gtk.TreeViewColumn(None, renderer, text=column_index)
    column_view.set_clickable(True)

    column_widget = gtk.VBox()
    column_widget.show()

    column_align = gtk.Alignment(0, 0, 0, 0)
    column_align.show()
    column_widget.pack_start(column_align)
    column_label = gtk.Label(column_title)
    column_label.show()
    column_align.add(column_label)

    column_entry = gtk.Entry()
    column_entry.set_name('treeview-header-entry')
    column_entry.show()
    column_widget.pack_start(column_entry)

    column_view.set_widget(column_widget)
    items_view.append_column(column_view)

# Setup column headers.
columns = items_view.get_columns()
for column in columns:
    column_widget = column.get_widget()
    column_header = find_closest_ancestor(column_widget, gtk.Button)
    if column_header:
        column_header.connect('focus-in-event', propagate_focus_in_event)
        column_header.connect('button-press-event', propagate_button_press_event)
        column_header.set_focus_on_click(False)
Pique answered 9/3, 2011 at 18:16 Comment(0)
S
4

The API has evolved since this question was asked, so I thought I would post an updated answer. (I had stumbled across this while dealing with a similar issue, although in my case I was trying to put two Buttons in the column header, not an Entry.)

First, some background. As mentioned in the question's edit, the issue stems from the way a TreeViewColumn is structured. The header of the column is a Button, and when you set_widget, that widget becomes a descendant of the Button. (This can be easily overlooked since the header does not respond like a button unless you set the column to be clickable. Also, the documentation does not help, as it seems to assume everyone already knows this.) A further cause of the issue is the way Buttons collect events. Unlike most widgets that respond to events, a Button does not have its own spot in the Gdk.Window hierarchy. Instead, it creates a special event window when it is realized. The method for accessing this window is Button-specific: get_event_window (distinct from the more generic get_window and get_parent_window). This event window sits invisibly above the Button, collecting events before they trickle down to any descendants of the Button. Hence, the widget you place in the column header does not receive the events required for interactivity.

The accepted solution is one way around this obstacle, and it was a worthy answer at the time. However, there is now an easier way. (I should mention that this is a GTK+ issue, independent of the language binding being used. Personally, I was using the C++ binding. I also peeked at the GTK+ source files – in C – to confirm that this is core GTK+ behavior and not some artifact of the binding.)

1) Find the header Button.

If column is the TreeViewColumn in question, the API for getting the button is now simply:

header_button = column.get_button()

The get_button method was added in version 3.0, which was tagged about six months after this question was asked. So close.

2) Propagate events from the Button to the Entry.

It took another four years (version 3.18) for this step to simplify. The key development was set_pass_through, which can tell the event window to let events pass through. As the documentation states: "In the terminology of the web this would be called 'pointer-events: none'."

def pass_through_event_window(button, event):
    if not isinstance(button, gtk.Button):
        raise TypeError("%r is not a gtk.Button" % button)
    event_window = button.get_event_window()
    event_window.set_pass_through(True)

The remaining trick is one of timing. The event window is not created until the Button is realized, so connecting to the Button's realize signal is in order.

header_button.connect('realize', pass_through_event_window)

And that's it (there is no step 3). Events will now propagate to the Entry or whatever widget you put in the column header.

My apologies if I messed up the syntax; I am translating from the C++ binding. If there are errors, I would request a kindly Python guru to correct them.

Serieswound answered 9/4, 2019 at 4:1 Comment(1)
The syntax looks correct. With GTK 3 I know the Python bindings changed from pygtk to pygobject which I haven't used. I would expect the API to match up.Pique
G
0

Unless this is not your answer, but i suggest you to use the standard and common method: using treeview.set_search_column(COLUMN_INDEX), no entry will be displayed by default (and makes UI cleaner) but when the user starts typing (with focus on that treeview) a popup input entry will be displayed, and searching and filtering will be done by GTK itself automatically.

If you persist that the search entry should be always visible, remove treeview header using treeview.set_headers_visible(False), and add a custom HBox (containing label and entry) above the treeview.

Grot answered 9/3, 2011 at 7:1 Comment(1)
I need to be able to search on multiple columns. Also, I can't use GTK's standard searching/filtering because the data is a subset fetched from a database and so when a filter is set I re-query a subset of the data with the filter.Pique

© 2022 - 2024 — McMap. All rights reserved.