gtk/gtk/gtkcolumnviewrowwidget.c
Benjamin Otte 3c451b3ec7 columnview: Make sure cells disconnect from their column
When a cell is removed from the columnview, we need to make sure it s
not just removed from the cell (via unset_parent()) but also from the
column.

Previously, we were doing this from dispose(), but this is broken
because dispose() only runs when the refcount goes to zero. But if some
code still has a reference for whatever reason, this won't happen.

So now we do it explicitly together with unset_parent().
2024-01-16 07:20:23 +01:00

601 lines
20 KiB
C

/*
* Copyright © 2023 Benjamin Otte
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include "config.h"
#include "gtkcolumnviewrowwidgetprivate.h"
#include "gtkbinlayout.h"
#include "gtkcolumnviewprivate.h"
#include "gtkcolumnviewcellwidgetprivate.h"
#include "gtkcolumnviewcolumnprivate.h"
#include "gtkcolumnviewrowprivate.h"
#include "gtkcolumnviewtitleprivate.h"
#include "gtklistitemfactoryprivate.h"
#include "gtklistbaseprivate.h"
#include "gtkwidget.h"
#include "gtkwidgetprivate.h"
G_DEFINE_TYPE (GtkColumnViewRowWidget, gtk_column_view_row_widget, GTK_TYPE_LIST_FACTORY_WIDGET)
static GtkColumnView *
gtk_column_view_row_widget_get_column_view (GtkColumnViewRowWidget *self)
{
GtkWidget *parent = _gtk_widget_get_parent (GTK_WIDGET (self));
if (GTK_IS_COLUMN_VIEW (parent))
return GTK_COLUMN_VIEW (parent);
parent = _gtk_widget_get_parent (parent);
g_assert (GTK_IS_COLUMN_VIEW (parent));
return GTK_COLUMN_VIEW (parent);
}
static gboolean
gtk_column_view_row_widget_is_header (GtkColumnViewRowWidget *self)
{
return gtk_widget_get_css_name (GTK_WIDGET (self)) == g_intern_static_string ("header");
}
static GtkColumnViewColumn *
gtk_column_view_row_child_get_column (GtkWidget *child)
{
if (GTK_IS_COLUMN_VIEW_CELL_WIDGET (child))
return gtk_column_view_cell_widget_get_column (GTK_COLUMN_VIEW_CELL_WIDGET (child));
else
return gtk_column_view_title_get_column (GTK_COLUMN_VIEW_TITLE (child));
g_return_val_if_reached (NULL);
}
static GtkWidget *
gtk_column_view_row_widget_find_child (GtkColumnViewRowWidget *self,
GtkColumnViewColumn *column)
{
GtkWidget *child;
for (child = gtk_widget_get_first_child (GTK_WIDGET (self));
child;
child = gtk_widget_get_next_sibling (child))
{
if (gtk_column_view_row_child_get_column (child) == column)
return child;
}
return NULL;
}
static void
gtk_column_view_row_widget_update (GtkListItemBase *base,
guint position,
gpointer item,
gboolean selected)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (base);
GtkWidget *child;
if (gtk_column_view_row_widget_is_header (self))
return;
GTK_LIST_ITEM_BASE_CLASS (gtk_column_view_row_widget_parent_class)->update (base, position, item, selected);
for (child = gtk_widget_get_first_child (GTK_WIDGET (self));
child;
child = gtk_widget_get_next_sibling (child))
{
gtk_list_item_base_update (GTK_LIST_ITEM_BASE (child), position, item, selected);
}
}
static gpointer
gtk_column_view_row_widget_create_object (GtkListFactoryWidget *fw)
{
return gtk_column_view_row_new ();
}
static void
gtk_column_view_row_widget_setup_object (GtkListFactoryWidget *fw,
gpointer object)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (fw);
GtkColumnViewRow *row = object;
g_assert (!gtk_column_view_row_widget_is_header (self));
GTK_LIST_FACTORY_WIDGET_CLASS (gtk_column_view_row_widget_parent_class)->setup_object (fw, object);
row->owner = self;
gtk_list_factory_widget_set_activatable (fw, row->activatable);
gtk_list_factory_widget_set_selectable (fw, row->selectable);
gtk_widget_set_focusable (GTK_WIDGET (self), row->focusable);
gtk_accessible_update_property (GTK_ACCESSIBLE (self),
GTK_ACCESSIBLE_PROPERTY_LABEL, row->accessible_label,
GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, row->accessible_description,
-1);
gtk_column_view_row_do_notify (row,
gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (self)) != NULL,
gtk_list_item_base_get_position (GTK_LIST_ITEM_BASE (self)) != GTK_INVALID_LIST_POSITION,
gtk_list_item_base_get_selected (GTK_LIST_ITEM_BASE (self)));
}
static void
gtk_column_view_row_widget_teardown_object (GtkListFactoryWidget *fw,
gpointer object)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (fw);
GtkColumnViewRow *row = object;
g_assert (!gtk_column_view_row_widget_is_header (self));
GTK_LIST_FACTORY_WIDGET_CLASS (gtk_column_view_row_widget_parent_class)->teardown_object (fw, object);
row->owner = NULL;
gtk_list_factory_widget_set_activatable (fw, FALSE);
gtk_list_factory_widget_set_selectable (fw, FALSE);
gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
gtk_accessible_reset_property (GTK_ACCESSIBLE (self), GTK_ACCESSIBLE_PROPERTY_LABEL);
gtk_accessible_reset_property (GTK_ACCESSIBLE (self), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION);
gtk_column_view_row_do_notify (row,
gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (self)) != NULL,
gtk_list_item_base_get_position (GTK_LIST_ITEM_BASE (self)) != GTK_INVALID_LIST_POSITION,
gtk_list_item_base_get_selected (GTK_LIST_ITEM_BASE (self)));
}
static void
gtk_column_view_row_widget_update_object (GtkListFactoryWidget *fw,
gpointer object,
guint position,
gpointer item,
gboolean selected)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (fw);
GtkListItemBase *base = GTK_LIST_ITEM_BASE (self);
GtkColumnViewRow *row = object;
/* Track notify manually instead of freeze/thaw_notify for performance reasons. */
gboolean notify_item = FALSE, notify_position = FALSE, notify_selected = FALSE;
g_assert (!gtk_column_view_row_widget_is_header (self));
/* FIXME: It's kinda evil to notify external objects from here... */
notify_item = gtk_list_item_base_get_item (base) != item;
notify_position = gtk_list_item_base_get_position (base) != position;
notify_selected = gtk_list_item_base_get_selected (base) != selected;
GTK_LIST_FACTORY_WIDGET_CLASS (gtk_column_view_row_widget_parent_class)->update_object (fw,
object,
position,
item,
selected);
if (row)
gtk_column_view_row_do_notify (row, notify_item, notify_position, notify_selected);
}
static GtkWidget *
gtk_column_view_next_focus_widget (GtkWidget *widget,
GtkWidget *current,
GtkDirectionType direction)
{
gboolean forward;
switch (direction)
{
case GTK_DIR_TAB_FORWARD:
forward = TRUE;
break;
case GTK_DIR_TAB_BACKWARD:
forward = FALSE;
break;
case GTK_DIR_LEFT:
forward = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
break;
case GTK_DIR_RIGHT:
forward = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL;
break;
case GTK_DIR_UP:
case GTK_DIR_DOWN:
return NULL;
default:
g_return_val_if_reached (NULL);
}
if (forward)
{
if (current == NULL)
return widget;
else if (current == widget)
return gtk_widget_get_first_child (widget);
else
return gtk_widget_get_next_sibling (current);
}
else
{
if (current == NULL)
return gtk_widget_get_last_child (widget);
else if (current == widget)
return NULL;
else
{
current = gtk_widget_get_prev_sibling (current);
if (current)
return current;
else
return widget;
}
}
}
static gboolean
gtk_column_view_row_widget_focus (GtkWidget *widget,
GtkDirectionType direction)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (widget);
GtkWidget *child, *current;
GtkColumnView *view;
current = gtk_widget_get_focus_child (widget);
view = gtk_column_view_row_widget_get_column_view (self);
if (gtk_column_view_get_tab_behavior (view) == GTK_LIST_TAB_CELL &&
(direction == GTK_DIR_TAB_FORWARD || direction == GTK_DIR_TAB_BACKWARD))
{
if (current || gtk_widget_is_focus (widget))
return FALSE;
}
if (current == NULL)
{
GtkColumnViewColumn *focus_column = gtk_column_view_get_focus_column (view);
if (focus_column)
{
current = gtk_column_view_row_widget_find_child (self, focus_column);
if (current && gtk_widget_child_focus (current, direction))
return TRUE;
}
}
if (gtk_widget_is_focus (widget))
current = widget;
for (child = gtk_column_view_next_focus_widget (widget, current, direction);
child;
child = gtk_column_view_next_focus_widget (widget, child, direction))
{
if (child == widget)
{
if (gtk_widget_grab_focus_self (widget))
{
gtk_column_view_set_focus_column (view, NULL, FALSE);
return TRUE;
}
}
else if (child)
{
if (gtk_widget_child_focus (child, direction))
return TRUE;
}
}
return FALSE;
}
static gboolean
gtk_column_view_row_widget_grab_focus (GtkWidget *widget)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (widget);
GtkWidget *child, *focus_child;
GtkColumnViewColumn *focus_column;
GtkColumnView *view;
view = gtk_column_view_row_widget_get_column_view (self);
focus_column = gtk_column_view_get_focus_column (view);
if (focus_column)
{
focus_child = gtk_column_view_row_widget_find_child (self, focus_column);
if (focus_child && gtk_widget_grab_focus (focus_child))
return TRUE;
}
else
focus_child = NULL;
if (gtk_widget_grab_focus_self (widget))
{
gtk_column_view_set_focus_column (view, NULL, FALSE);
return TRUE;
}
for (child = focus_child ? gtk_widget_get_next_sibling (focus_child) : gtk_widget_get_first_child (widget);
child != focus_child;
child = child ? gtk_widget_get_next_sibling (child) : gtk_widget_get_first_child (widget))
{
/* When we started iterating at focus_child, we want to iterate over the rest
* of the children, too */
if (child == NULL)
continue;
if (gtk_widget_grab_focus (child))
return TRUE;
}
return FALSE;
}
static void
gtk_column_view_row_widget_set_focus_child (GtkWidget *widget,
GtkWidget *child)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (widget);
GTK_WIDGET_CLASS (gtk_column_view_row_widget_parent_class)->set_focus_child (widget, child);
if (child)
{
gtk_column_view_set_focus_column (gtk_column_view_row_widget_get_column_view (self),
gtk_column_view_row_child_get_column (child),
TRUE);
}
}
static void
gtk_column_view_row_widget_dispose (GObject *object)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (object);
GtkWidget *child;
while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))))
{
gtk_column_view_row_widget_remove_child (self, child);
}
G_OBJECT_CLASS (gtk_column_view_row_widget_parent_class)->dispose (object);
}
static void
gtk_column_view_row_widget_measure_along (GtkColumnViewRowWidget *self,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
GtkOrientation orientation = GTK_ORIENTATION_VERTICAL;
GtkColumnView *view;
GtkWidget *child;
guint i, n;
GtkRequestedSize *sizes = NULL;
view = gtk_column_view_row_widget_get_column_view (self);
if (for_size > -1)
{
n = g_list_model_get_n_items (gtk_column_view_get_columns (view));
sizes = g_newa (GtkRequestedSize, n);
gtk_column_view_distribute_width (view, for_size, sizes);
}
for (child = _gtk_widget_get_first_child (GTK_WIDGET (self)), i = 0;
child != NULL;
child = _gtk_widget_get_next_sibling (child), i++)
{
int child_min = 0;
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
if (!gtk_widget_should_layout (child))
continue;
gtk_widget_measure (child, orientation,
for_size > -1 ? sizes[i].minimum_size : -1,
&child_min, &child_nat,
&child_min_baseline, &child_nat_baseline);
*minimum = MAX (*minimum, child_min);
*natural = MAX (*natural, child_nat);
if (child_min_baseline > -1)
*minimum_baseline = MAX (*minimum_baseline, child_min_baseline);
if (child_nat_baseline > -1)
*natural_baseline = MAX (*natural_baseline, child_nat_baseline);
}
}
static void
gtk_column_view_row_widget_measure (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
GtkColumnViewRowWidget *self = GTK_COLUMN_VIEW_ROW_WIDGET (widget);
if (orientation == GTK_ORIENTATION_HORIZONTAL)
{
gtk_column_view_measure_across (gtk_column_view_row_widget_get_column_view (self),
minimum,
natural);
}
else
{
gtk_column_view_row_widget_measure_along (self,
for_size,
minimum,
natural,
minimum_baseline,
natural_baseline);
}
}
static void
gtk_column_view_row_widget_allocate (GtkWidget *widget,
int width,
int height,
int baseline)
{
GtkWidget *child;
for (child = _gtk_widget_get_first_child (widget);
child != NULL;
child = _gtk_widget_get_next_sibling (child))
{
GtkColumnViewColumn *column;
int col_x, col_width, min;
if (!gtk_widget_should_layout (child))
continue;
column = gtk_column_view_row_child_get_column (child);
gtk_column_view_column_get_header_allocation (column, &col_x, &col_width);
gtk_widget_measure (child, GTK_ORIENTATION_HORIZONTAL, -1, &min, NULL, NULL, NULL);
gtk_widget_size_allocate (child, &(GtkAllocation) { col_x, 0, MAX (min, col_width), height }, baseline);
}
}
static void
add_arrow_bindings (GtkWidgetClass *widget_class,
guint keysym,
GtkDirectionType direction)
{
guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
gtk_widget_class_add_binding_signal (widget_class, keysym, 0,
"move-focus",
"(i)",
direction);
gtk_widget_class_add_binding_signal (widget_class, keysym, GDK_CONTROL_MASK,
"move-focus",
"(i)",
direction);
gtk_widget_class_add_binding_signal (widget_class, keypad_keysym, 0,
"move-focus",
"(i)",
direction);
gtk_widget_class_add_binding_signal (widget_class, keypad_keysym, GDK_CONTROL_MASK,
"move-focus",
"(i)",
direction);
}
static void
gtk_column_view_row_widget_class_init (GtkColumnViewRowWidgetClass *klass)
{
GtkListFactoryWidgetClass *factory_class = GTK_LIST_FACTORY_WIDGET_CLASS (klass);
GtkListItemBaseClass *base_class = GTK_LIST_ITEM_BASE_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
factory_class->create_object = gtk_column_view_row_widget_create_object;
factory_class->setup_object = gtk_column_view_row_widget_setup_object;
factory_class->update_object = gtk_column_view_row_widget_update_object;
factory_class->teardown_object = gtk_column_view_row_widget_teardown_object;
base_class->update = gtk_column_view_row_widget_update;
widget_class->focus = gtk_column_view_row_widget_focus;
widget_class->grab_focus = gtk_column_view_row_widget_grab_focus;
widget_class->set_focus_child = gtk_column_view_row_widget_set_focus_child;
widget_class->measure = gtk_column_view_row_widget_measure;
widget_class->size_allocate = gtk_column_view_row_widget_allocate;
object_class->dispose = gtk_column_view_row_widget_dispose;
add_arrow_bindings (widget_class, GDK_KEY_Left, GTK_DIR_LEFT);
add_arrow_bindings (widget_class, GDK_KEY_Right, GTK_DIR_RIGHT);
/* This gets overwritten by gtk_column_view_row_widget_new() but better safe than sorry */
gtk_widget_class_set_css_name (widget_class, g_intern_static_string ("row"));
gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_ROW);
}
static void
gtk_column_view_row_widget_init (GtkColumnViewRowWidget *self)
{
gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
}
GtkWidget *
gtk_column_view_row_widget_new (GtkListItemFactory *factory,
gboolean is_header)
{
return g_object_new (GTK_TYPE_COLUMN_VIEW_ROW_WIDGET,
"factory", factory,
"css-name", is_header ? "header" : "row",
"selectable", TRUE,
"activatable", TRUE,
NULL);
}
void
gtk_column_view_row_widget_add_child (GtkColumnViewRowWidget *self,
GtkWidget *child)
{
gtk_widget_set_parent (child, GTK_WIDGET (self));
}
void
gtk_column_view_row_widget_reorder_child (GtkColumnViewRowWidget *self,
GtkWidget *child,
guint position)
{
GtkWidget *widget = GTK_WIDGET (self);
GtkWidget *sibling = NULL;
if (position > 0)
{
GtkWidget *c;
guint i;
for (c = gtk_widget_get_first_child (widget), i = 0;
c;
c = gtk_widget_get_next_sibling (c), i++)
{
if (i + 1 == position)
{
sibling = c;
break;
}
}
}
if (child != sibling)
gtk_widget_insert_after (child, widget, sibling);
}
void
gtk_column_view_row_widget_remove_child (GtkColumnViewRowWidget *self,
GtkWidget *child)
{
if (GTK_IS_COLUMN_VIEW_CELL_WIDGET (child))
gtk_column_view_cell_widget_unset_column (GTK_COLUMN_VIEW_CELL_WIDGET (child));
gtk_widget_unparent (child);
}