gtk2/gtk/gtktreeexpander.c
Matthias Clasen 9555e611e1 treeexpander: Auto-expand during DND
When hovering over a tree expander during DND,
expand the tree after a timeout. This matches
the behavior of GtkTreeView and GtkExpander.
2020-06-19 15:26:47 -04:00

759 lines
23 KiB
C

/*
* Copyright © 2019 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 "gtktreeexpander.h"
#include "gtkboxlayout.h"
#include "gtkbuiltiniconprivate.h"
#include "gtkdropcontrollermotion.h"
#include "gtkgestureclick.h"
#include "gtkintl.h"
#include "gtktreelistmodel.h"
/**
* SECTION:gtktreeexpander
* @title: GtkTreeExpander
* @short_description: An indenting expander button for use in a tree list
* @see_also: #GtkTreeListModel
*
* GtkTreeExpander is a widget that provides an expander for a list.
*
* It is typically placed as a bottommost child into a #GtkListView to allow
* users to expand and collapse children in a list with a #GtkTreeListModel.
* It will provide the common UI elements, gestures and keybindings for this
* purpose.
*
* On top of this, the "listitem.expand", "listitem.collapse" and
* "listitem.toggle-expand" actions are provided to allow adding custom UI
* for managing expanded state.
*
* The #GtkTreeListModel must be set to not be passthrough. Then it will provide
* #GtkTreeListRow items which can be set via gtk_tree_expander_set_list_row()
* on the expander. The expander will then watch that row item automatically.
* gtk_tree_expander_set_child() sets the widget that displays the actual row
* contents.
*
* # CSS nodes
*
* |[<!-- language="plain" -->
* treeexpander
* ├── [indent]*
* ├── [expander]
* ╰── <child>
* ]|
*
* GtkTreeExpander has zero or one CSS nodes with the name "expander" that should
* display the expander icon. The node will be `:checked` when it is expanded.
* If the node is not expandable, an "indent" node will be displayed instead.
*
* For every level of depth, another "indent" node is prepended.
*/
struct _GtkTreeExpander
{
GtkWidget parent_instance;
GtkTreeListRow *list_row;
GtkWidget *child;
GtkWidget *expander;
guint notify_handler;
guint expand_timer;
};
enum
{
PROP_0,
PROP_CHILD,
PROP_ITEM,
PROP_LIST_ROW,
N_PROPS
};
G_DEFINE_TYPE (GtkTreeExpander, gtk_tree_expander, GTK_TYPE_WIDGET)
static GParamSpec *properties[N_PROPS] = { NULL, };
static void
gtk_tree_expander_click_gesture_pressed (GtkGestureClick *gesture,
int n_press,
double x,
double y,
gpointer unused)
{
GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
gtk_widget_activate_action (widget, "listitem.toggle-expand", NULL);
gtk_widget_set_state_flags (widget,
GTK_STATE_FLAG_ACTIVE,
FALSE);
}
static void
gtk_tree_expander_click_gesture_released (GtkGestureClick *gesture,
int n_press,
double x,
double y,
gpointer unused)
{
gtk_widget_unset_state_flags (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)),
GTK_STATE_FLAG_ACTIVE);
}
static void
gtk_tree_expander_click_gesture_canceled (GtkGestureClick *gesture,
GdkEventSequence *sequence,
gpointer unused)
{
gtk_widget_unset_state_flags (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)),
GTK_STATE_FLAG_ACTIVE);
}
static void
gtk_tree_expander_update_for_list_row (GtkTreeExpander *self)
{
if (self->list_row == NULL)
{
GtkWidget *child;
for (child = gtk_widget_get_first_child (GTK_WIDGET (self));
child != self->child;
child = gtk_widget_get_first_child (GTK_WIDGET (self)))
{
gtk_widget_unparent (child);
}
self->expander = NULL;
}
else
{
GtkWidget *child;
guint i, depth;
depth = gtk_tree_list_row_get_depth (self->list_row);
if (gtk_tree_list_row_is_expandable (self->list_row))
{
if (self->expander == NULL)
{
GtkGesture *gesture;
self->expander = gtk_builtin_icon_new ("expander");
gesture = gtk_gesture_click_new ();
gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture),
GTK_PHASE_BUBBLE);
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture),
FALSE);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture),
GDK_BUTTON_PRIMARY);
g_signal_connect (gesture, "pressed",
G_CALLBACK (gtk_tree_expander_click_gesture_pressed), NULL);
g_signal_connect (gesture, "released",
G_CALLBACK (gtk_tree_expander_click_gesture_released), NULL);
g_signal_connect (gesture, "cancel",
G_CALLBACK (gtk_tree_expander_click_gesture_canceled), NULL);
gtk_widget_add_controller (self->expander, GTK_EVENT_CONTROLLER (gesture));
gtk_widget_insert_before (self->expander,
GTK_WIDGET (self),
self->child);
}
if (gtk_tree_list_row_get_expanded (self->list_row))
gtk_widget_set_state_flags (self->expander, GTK_STATE_FLAG_CHECKED, FALSE);
else
gtk_widget_unset_state_flags (self->expander, GTK_STATE_FLAG_CHECKED);
child = gtk_widget_get_prev_sibling (self->expander);
}
else
{
g_clear_pointer (&self->expander, gtk_widget_unparent);
depth++;
if (self->child)
child = gtk_widget_get_prev_sibling (self->child);
else
child = gtk_widget_get_last_child (GTK_WIDGET (self));
}
for (i = 0; i < depth; i++)
{
if (child)
child = gtk_widget_get_prev_sibling (child);
else
gtk_widget_insert_after (gtk_builtin_icon_new ("indent"), GTK_WIDGET (self), NULL);
}
while (child)
{
GtkWidget *prev = gtk_widget_get_prev_sibling (child);
gtk_widget_unparent (child);
child = prev;
}
}
}
static void
gtk_tree_expander_list_row_notify_cb (GtkTreeListRow *list_row,
GParamSpec *pspec,
GtkTreeExpander *self)
{
if (pspec->name == g_intern_static_string ("expanded"))
{
if (self->expander)
{
if (gtk_tree_list_row_get_expanded (list_row))
gtk_widget_set_state_flags (self->expander, GTK_STATE_FLAG_CHECKED, FALSE);
else
gtk_widget_unset_state_flags (self->expander, GTK_STATE_FLAG_CHECKED);
}
}
else if (pspec->name == g_intern_static_string ("item"))
{
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ITEM]);
}
else
{
/* can this happen other than when destroying the row? */
gtk_tree_expander_update_for_list_row (self);
}
}
static gboolean
gtk_tree_expander_focus (GtkWidget *widget,
GtkDirectionType direction)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
/* The idea of this function is the following:
* 1. If any child can take focus, do not ever attempt
* to take focus.
* 2. Otherwise, if this item is selectable or activatable,
* allow focusing this widget.
*
* This makes sure every item in a list is focusable for
* activation and selection handling, but no useless widgets
* get focused and moving focus is as fast as possible.
*/
if (self->child)
{
if (gtk_widget_get_focus_child (widget))
return FALSE;
if (gtk_widget_child_focus (self->child, direction))
return TRUE;
}
if (gtk_widget_is_focus (widget))
return FALSE;
if (!gtk_widget_get_can_focus (widget))
return FALSE;
gtk_widget_grab_focus (widget);
return TRUE;
}
static gboolean
gtk_tree_expander_grab_focus (GtkWidget *widget)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
if (self->child && gtk_widget_grab_focus (self->child))
return TRUE;
return GTK_WIDGET_CLASS (gtk_tree_expander_parent_class)->grab_focus (widget);
}
static void
gtk_tree_expander_clear_list_row (GtkTreeExpander *self)
{
if (self->list_row == NULL)
return;
g_signal_handler_disconnect (self->list_row, self->notify_handler);
self->notify_handler = 0;
g_clear_object (&self->list_row);
}
static void
gtk_tree_expander_dispose (GObject *object)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (object);
if (self->expand_timer)
{
g_source_remove (self->expand_timer);
self->expand_timer = 0;
}
gtk_tree_expander_clear_list_row (self);
gtk_tree_expander_update_for_list_row (self);
g_clear_pointer (&self->child, gtk_widget_unparent);
g_assert (self->expander == NULL);
G_OBJECT_CLASS (gtk_tree_expander_parent_class)->dispose (object);
}
static void
gtk_tree_expander_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (object);
switch (property_id)
{
case PROP_CHILD:
g_value_set_object (value, self->child);
break;
case PROP_ITEM:
g_value_set_object (value, gtk_tree_expander_get_item (self));
break;
case PROP_LIST_ROW:
g_value_set_object (value, self->list_row);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_tree_expander_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (object);
switch (property_id)
{
case PROP_CHILD:
gtk_tree_expander_set_child (self, g_value_get_object (value));
break;
case PROP_LIST_ROW:
gtk_tree_expander_set_list_row (self, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_tree_expander_expand (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
if (self->list_row == NULL)
return;
gtk_tree_list_row_set_expanded (self->list_row, TRUE);
}
static void
gtk_tree_expander_collapse (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
if (self->list_row == NULL)
return;
gtk_tree_list_row_set_expanded (self->list_row, FALSE);
}
static void
gtk_tree_expander_toggle_expand (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
if (self->list_row == NULL)
return;
gtk_tree_list_row_set_expanded (self->list_row, !gtk_tree_list_row_get_expanded (self->list_row));
}
static gboolean
expand_collapse_right (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
if (self->list_row == NULL)
return FALSE;
gtk_tree_list_row_set_expanded (self->list_row, gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL);
return TRUE;
}
static gboolean
expand_collapse_left (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (widget);
if (self->list_row == NULL)
return FALSE;
gtk_tree_list_row_set_expanded (self->list_row, gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL);
return TRUE;
}
static void
gtk_tree_expander_class_init (GtkTreeExpanderClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
widget_class->focus = gtk_tree_expander_focus;
widget_class->grab_focus = gtk_tree_expander_grab_focus;
gobject_class->dispose = gtk_tree_expander_dispose;
gobject_class->get_property = gtk_tree_expander_get_property;
gobject_class->set_property = gtk_tree_expander_set_property;
/**
* GtkTreeExpander:child:
*
* The child widget with the actual contents
*/
properties[PROP_CHILD] =
g_param_spec_object ("child",
P_("Child"),
P_("The child widget with the actual contents"),
GTK_TYPE_WIDGET,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* GtkTreeExpander:item:
*
* The item held by this expander's row
*/
properties[PROP_ITEM] =
g_param_spec_object ("item",
P_("Item"),
P_("The item held by this expander's row"),
G_TYPE_OBJECT,
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
/**
* GtkTreeExpander:list-row:
*
* The list row to track for expander state
*/
properties[PROP_LIST_ROW] =
g_param_spec_object ("list-row",
P_("List row"),
P_("The list row to track for expander state"),
GTK_TYPE_TREE_LIST_ROW,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (gobject_class, N_PROPS, properties);
/**
* GtkTreeExpander|listitem.expand:
*
* Expands the expander if it can be expanded.
*/
gtk_widget_class_install_action (widget_class,
"listitem.expand",
NULL,
gtk_tree_expander_expand);
/**
* GtkTreeExpander|listitem.collapse:
*
* Collapses the expander.
*/
gtk_widget_class_install_action (widget_class,
"listitem.collapse",
NULL,
gtk_tree_expander_collapse);
/**
* GtkTreeExpander|listitem.toggle-expand:
*
* Tries to expand the expander if it was collapsed or collapses it if
* it was expanded.
*/
gtk_widget_class_install_action (widget_class,
"listitem.toggle-expand",
NULL,
gtk_tree_expander_toggle_expand);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_plus, 0,
"listitem.expand", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Add, 0,
"listitem.expand", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_asterisk, 0,
"listitem.expand", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Multiply, 0,
"listitem.expand", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_minus, 0,
"listitem.collapse", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Subtract, 0,
"listitem.collapse", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_slash, 0,
"listitem.collapse", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Divide, 0,
"listitem.collapse", NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_Right, GDK_SHIFT_MASK,
expand_collapse_right, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Right, GDK_SHIFT_MASK,
expand_collapse_right, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_Right, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
expand_collapse_right, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Right, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
expand_collapse_right, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_Left, GDK_SHIFT_MASK,
expand_collapse_left, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Left, GDK_SHIFT_MASK,
expand_collapse_left, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_Left, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
expand_collapse_left, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Left, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
expand_collapse_left, NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_space, GDK_CONTROL_MASK,
"listitem.toggle-expand", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Space, GDK_CONTROL_MASK,
"listitem.toggle-expand", NULL);
#if 0
/* These can't be implementes yet. */
gtk_widget_class_add_binding (widget_class, GDK_KEY_BackSpace, 0, go_to_parent_row, NULL, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_BackSpace, GDK_CONTROL_MASK, go_to_parent_row, NULL, NULL);
#endif
gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
gtk_widget_class_set_css_name (widget_class, I_("treeexpander"));
}
static gboolean
gtk_tree_expander_expand_timeout (gpointer data)
{
GtkTreeExpander *self = GTK_TREE_EXPANDER (data);
if (self->list_row != NULL)
gtk_tree_list_row_set_expanded (self->list_row, TRUE);
self->expand_timer = 0;
return G_SOURCE_REMOVE;
}
#define TIMEOUT_EXPAND 500
static void
gtk_tree_expander_drag_enter (GtkDropControllerMotion *motion,
double x,
double y,
GtkTreeExpander *self)
{
if (self->list_row == NULL)
return;
if (!gtk_tree_list_row_get_expanded (self->list_row) &&
!self->expand_timer)
{
self->expand_timer = g_timeout_add (TIMEOUT_EXPAND, (GSourceFunc) gtk_tree_expander_expand_timeout, self);
g_source_set_name_by_id (self->expand_timer, "[gtk] gtk_tree_expander_expand_timeout");
}
}
static void
gtk_tree_expander_drag_leave (GtkDropControllerMotion *motion,
GtkTreeExpander *self)
{
if (self->expand_timer)
{
g_source_remove (self->expand_timer);
self->expand_timer = 0;
}
}
static void
gtk_tree_expander_init (GtkTreeExpander *self)
{
GtkEventController *controller;
gtk_widget_set_can_focus (GTK_WIDGET (self), TRUE);
controller = gtk_drop_controller_motion_new ();
g_signal_connect (controller, "enter", G_CALLBACK (gtk_tree_expander_drag_enter), self);
g_signal_connect (controller, "leave", G_CALLBACK (gtk_tree_expander_drag_leave), self);
gtk_widget_add_controller (GTK_WIDGET (self), controller);
}
/**
* gtk_tree_expander_new:
*
* Creates a new #GtkTreeExpander
*
* Returns: a new #GtkTreeExpander
**/
GtkWidget *
gtk_tree_expander_new (void)
{
return g_object_new (GTK_TYPE_TREE_EXPANDER,
NULL);
}
/**
* gtk_tree_expander_get_child
* @self: a #GtkTreeExpander
*
* Gets the child widget displayed by @self.
*
* Returns: (nullable) (transfer none): The child displayed by @self
**/
GtkWidget *
gtk_tree_expander_get_child (GtkTreeExpander *self)
{
g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL);
return self->child;
}
/**
* gtk_tree_expander_set_child:
* @self: a #GtkTreeExpander widget
* @child: (nullable): a #GtkWidget, or %NULL
*
* Sets the content widget to display.
*/
void
gtk_tree_expander_set_child (GtkTreeExpander *self,
GtkWidget *child)
{
g_return_if_fail (GTK_IS_TREE_EXPANDER (self));
g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));
if (self->child == child)
return;
g_clear_pointer (&self->child, gtk_widget_unparent);
if (child)
{
self->child = child;
gtk_widget_set_parent (child, GTK_WIDGET (self));
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CHILD]);
}
/**
* gtk_tree_expander_get_item
* @self: a #GtkTreeExpander
*
* Forwards the item set on the #GtkTreeListRow that @self is managing.
*
* This call is essentially equivalent to calling
* `gtk_tree_list_row_get_item (gtk_tree_expander_get_list_row (@self))`.
*
* Returns: (nullable) (transfer none): The item of the row
**/
gpointer
gtk_tree_expander_get_item (GtkTreeExpander *self)
{
g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL);
if (self->list_row == NULL)
return NULL;
return gtk_tree_list_row_get_item (self->list_row);
}
/**
* gtk_tree_expander_get_list_row
* @self: a #GtkTreeExpander
*
* Gets the list row managed by @self.
*
* Returns: (nullable) (transfer none): The list row displayed by @self
**/
GtkTreeListRow *
gtk_tree_expander_get_list_row (GtkTreeExpander *self)
{
g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL);
return self->list_row;
}
/**
* gtk_tree_expander_set_list_row:
* @self: a #GtkTreeExpander widget
* @list_row: (nullable): a #GtkTreeListRow, or %NULL
*
* Sets the tree list row that this expander should manage.
*/
void
gtk_tree_expander_set_list_row (GtkTreeExpander *self,
GtkTreeListRow *list_row)
{
g_return_if_fail (GTK_IS_TREE_EXPANDER (self));
g_return_if_fail (list_row == NULL || GTK_IS_TREE_LIST_ROW (list_row));
if (self->list_row == list_row)
return;
g_object_freeze_notify (G_OBJECT (self));
gtk_tree_expander_clear_list_row (self);
if (list_row)
{
self->list_row = g_object_ref (list_row);
self->notify_handler = g_signal_connect (list_row,
"notify",
G_CALLBACK (gtk_tree_expander_list_row_notify_cb),
self);
}
gtk_tree_expander_update_for_list_row (self);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LIST_ROW]);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ITEM]);
g_object_thaw_notify (G_OBJECT (self));
}