Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PyGTK Entry widget in TreeViewColumn header

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.

like image 694
Uyghur Lives Matter Avatar asked Mar 07 '11 18:03

Uyghur Lives Matter


2 Answers

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)
like image 55
Uyghur Lives Matter Avatar answered Nov 16 '22 01:11

Uyghur Lives Matter


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.

like image 44
JaMiT Avatar answered Nov 16 '22 01:11

JaMiT