Emitting signal through g_idle_add() callback broken for GtkTreeView widget
Steps to reproduce
To reproduce the issue, please use the small application attached to the end of the bug report. The application is designed to continuously update a GtkTreeModel through signals emitted by a g_idle_add_full() callback.
- Start the application,
- Click on any of the columns of the first row. When clicking on the 3 column of the first row, a message will be printed to the console.
- Click the "Go" button to start the GtkTreeModel update process.
- Again, click on any of the columns of the first row. You'll see that the same message as in 2 is now displayed when clicking on the second or even first columns
Current behavior
See step 4 above. The "button-press-event" signal seems to be triggering on rows other than the ones is was connected to.
Expected outcome
Only clicks on the third row should trigger the message display, regardless whether the GtkTreeModel is being updated or not.
Version information
Gtk: 3.22.30
OS: Ubuntu 18.04
Gtk/GLib installed from distribution.
Additional information
The column which is experiencing the issue is defined as having AUTOMATIC sizing. It appears that this issue is only application to that sizing policy. I have done some debugging and the results are described in https://mail.gnome.org/archives/gtk-list/2018-December/msg00005.html and https://mail.gnome.org/archives/gtk-list/2019-February/msg00061.html.
The second of the two posts above pinpoints where the issue is coming from. Below is the application, which reproduces the issue:
#include <gtk/gtk.h>
G_BEGIN_DECLS
#define CUSTOM_OBJECT_TYPE (custom_object_get_type())
G_DECLARE_FINAL_TYPE(CustomObject, custom_object, CUSTOM, OBJECT, GObject);
struct _CustomObject {
GObject parent;
GCancellable *cancel;
GTask *task;
};
G_END_DECLS
G_DEFINE_TYPE(CustomObject, custom_object, G_TYPE_OBJECT);
static guint signal_id;
typedef struct _signal_args {
const gchar *signal;
gpointer instance;
guint64 value;
} signal_args_t;
static gboolean signal_emitter(gpointer data)
{
signal_args_t *args = data;
g_signal_emit_by_name(args->instance, args->signal, args->value);
return FALSE;
}
void signal_emit(gpointer instance, const gchar *signal, guint64 value)
{
signal_args_t *args;
args = g_new0(signal_args_t, 1);
args->instance = instance;
args->signal = "updated";
args->value = value;
g_idle_add_full(G_PRIORITY_HIGH, signal_emitter, args,
(GDestroyNotify)g_free);
}
static void custom_object_finalize(GObject *object)
{
CustomObject *obj = CUSTOM_OBJECT(object);
G_OBJECT_CLASS(custom_object_parent_class)->finalize(object);
}
static void custom_object_class_init(CustomObjectClass *class)
{
GType params[] = {G_TYPE_UINT64};
G_OBJECT_CLASS(class)->finalize = custom_object_finalize;
signal_id = g_signal_newv(
"updated", G_TYPE_FROM_CLASS(class),
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
NULL, NULL, NULL, NULL, G_TYPE_NONE, 1, params);
}
static void custom_object_init(CustomObject *object)
{
}
static CustomObject *custom_object_new(void)
{
CustomObject *object;
object = g_object_new(CUSTOM_OBJECT_TYPE, NULL);
object->cancel = g_cancellable_new();
return object;
}
static void custom_object_worker(GTask *task, gpointer source, gpointer data,
GCancellable *cancel)
{
CustomObject *obj = CUSTOM_OBJECT(source);
guint64 value = 0;
while (!g_cancellable_is_cancelled(obj->cancel))
{
signal_emit(obj, "updated", ++value);
usleep(10);
}
}
static void custom_object_start(CustomObject *obj)
{
obj->task = g_task_new(obj, obj->cancel, NULL, NULL);
g_task_run_in_thread(obj->task, custom_object_worker);
}
static void custom_object_stop(CustomObject *obj)
{
g_cancellable_cancel(obj->cancel);
}
static void go(GtkButton *button, gpointer data)
{
CustomObject *obj = CUSTOM_OBJECT(data);
custom_object_start(obj);
}
static void on_custom_object_updated(CustomObject *obj, guint64 value,
gpointer data)
{
GtkTreeModel *model = GTK_TREE_MODEL(data);
GtkTreeIter iter;
gtk_tree_model_get_iter_first(model, &iter);
gtk_tree_store_set(GTK_TREE_STORE(model), &iter, 1, value, -1);
}
static gboolean on_tree_view_button_pressed(GtkWidget *widget, GdkEvent *event,
gpointer data)
{
GdkEventButton *button = (GdkEventButton *)event;
GtkTreePath *path;
GtkTreeModel *model = GTK_TREE_MODEL(data);
GtkTreeViewColumn *column;
if (button->type != GDK_BUTTON_PRESS ||
!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), button->x,
button->y, &path, &column, NULL,
NULL))
return FALSE;
if (strcmp(gtk_tree_view_column_get_title(column), "column 3"))
return FALSE;
printf("Third column activated\n");
}
static void renderer_data_func(GtkTreeViewColumn *column, GtkCellRenderer *cell,
GtkTreeModel *model, GtkTreeIter *iter,
gpointer data)
{
guint64 value;
gtk_tree_model_get(model, iter, 1, &value, -1);
if (GTK_IS_CELL_RENDERER_SPINNER(cell))
g_object_set(G_OBJECT(cell), "active", TRUE, "pulse", value, NULL);
}
int main(int argc, char **argv)
{
GtkBuilder *builder;
GtkWidget *window, *button;
GtkTreeModel *model;
GtkTreeView *view;
GtkTreeViewColumn *column;
GtkCellRenderer *renderer;
GList *renderers;
GError *error = NULL;
CustomObject *obj;
gtk_init(&argc, &argv);
builder = gtk_builder_new();
if (!gtk_builder_add_from_file(builder, "main.ui", &error))
{
g_printerr("Error: %s\n", error->message);
g_clear_error(&error);
return 1;
}
obj = custom_object_new();
window = GTK_WIDGET(gtk_builder_get_object(builder, "mainWindow"));
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
button = GTK_WIDGET(gtk_builder_get_object(builder, "goButton"));
g_signal_connect(button, "clicked", G_CALLBACK(go), obj);
model = GTK_TREE_MODEL(gtk_builder_get_object(builder, "treestore"));
g_signal_connect(obj, "updated", G_CALLBACK(on_custom_object_updated),
model);
view = GTK_TREE_VIEW(gtk_builder_get_object(builder, "treeView"));
g_signal_connect(view, "button-press-event",
G_CALLBACK(on_tree_view_button_pressed), model);
column = gtk_tree_view_get_column(view, 1);
renderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
renderer = GTK_CELL_RENDERER(g_list_first(renderers)->data);
gtk_tree_view_column_set_cell_data_func(column, renderer,
renderer_data_func, NULL, NULL);
gtk_tree_store_insert_with_values(GTK_TREE_STORE(model), NULL, NULL, 0,
0, "test entry", 1, 0, -1);
gtk_widget_show_all(GTK_WIDGET(window));
gtk_main();
return 0;
}
And this is the UI file used by the application:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkTreeStore" id="treestore">
<columns>
<!-- column-name column1 -->
<column type="gchararray"/>
<!-- column-name value -->
<column type="guint"/>
</columns>
</object>
<object class="GtkApplicationWindow" id="mainWindow">
<property name="can_focus">False</property>
<property name="default_width">440</property>
<property name="default_height">250</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton" id="goButton">
<property name="label" translatable="yes">GO</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="treeView">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">treestore</property>
<property name="expander_column">columnOne</property>
<property name="reorderable">True</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="columnOne">
<property name="resizable">True</property>
<property name="sizing">autosize</property>
<property name="title" translatable="yes">column 1</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<child>
<object class="GtkCellRendererText" id="columnOneRenderer"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="columnTwo">
<property name="sizing">fixed</property>
<property name="title" translatable="yes">column 2</property>
<child>
<object class="GtkCellRendererSpinner" id="columnTwoRenderer">
<property name="active">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="columnThree">
<property name="sizing">fixed</property>
<property name="title" translatable="yes">column 3</property>
<child>
<object class="GtkCellRendererPixbuf" id="columnThreeRenderer">
<property name="stock_id">gtk-edit</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>