gtk2/gtk/gtklistbase.c
António Fernandes 4fc4298920 listbase: Grab focus on items instead of container
The container view itself being focusable makes keyboard navigation
slower by adding a useless focus step.

It also means if an item gets removed, the focus jumps back to the view,
instead of jumping to the next item, as seen in nautilus bug report:
https://gitlab.gnome.org/GNOME/nautilus/-/issues/2489

Instead of making the GtkListBase container itself focusable, override
the .grab_focus() vfunc. This way, calling gtk_widget_grab_focus() on
the view container keeps working sucessfully, but focuses the focus
item directly instead.

This is particularly useful to have because applicaiton authors do
not have direct acess to this class's children, so they can't call
gtk_widget_grab_focus() on them directly.
2022-10-03 20:26:24 +01:00

2174 lines
73 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 "gtklistbaseprivate.h"
#include "gtkadjustment.h"
#include "gtkbitset.h"
#include "gtkdragsourceprivate.h"
#include "gtkdropcontrollermotion.h"
#include "gtkgesturedrag.h"
#include "gtkgizmoprivate.h"
#include "gtklistitemwidgetprivate.h"
#include "gtkmultiselection.h"
#include "gtkorientable.h"
#include "gtkscrollable.h"
#include "gtksingleselection.h"
#include "gtksnapshot.h"
#include "gtktypebuiltins.h"
#include "gtkwidgetprivate.h"
/* Allow shadows to overdraw without immediately culling the widget at the viewport
* boundary.
* Choose this so that roughly 1 extra widget gets drawn on each side of the viewport,
* but not more. Icons are 16px, text height is somewhere there, too.
*/
#define GTK_LIST_BASE_CHILD_MAX_OVERDRAW 10
typedef struct _RubberbandData RubberbandData;
struct _RubberbandData
{
GtkWidget *widget; /* The rubberband widget */
GtkListItemTracker *start_tracker; /* The item we started dragging on */
double start_align_across; /* alignment in horizontal direction */
double start_align_along; /* alignment in vertical direction */
double pointer_x, pointer_y; /* mouse coordinates in widget space */
};
typedef struct _GtkListBasePrivate GtkListBasePrivate;
struct _GtkListBasePrivate
{
GtkListItemManager *item_manager;
GtkSelectionModel *model;
GtkOrientation orientation;
GtkAdjustment *adjustment[2];
GtkScrollablePolicy scroll_policy[2];
GtkListItemTracker *anchor;
double anchor_align_along;
double anchor_align_across;
GtkPackType anchor_side_along;
GtkPackType anchor_side_across;
guint center_widgets;
guint above_below_widgets;
/* the last item that was selected - basically the location to extend selections from */
GtkListItemTracker *selected;
/* the item that has input focus */
GtkListItemTracker *focus;
gboolean enable_rubberband;
GtkGesture *drag_gesture;
RubberbandData *rubberband;
guint autoscroll_id;
double autoscroll_delta_x;
double autoscroll_delta_y;
};
enum
{
PROP_0,
PROP_HADJUSTMENT,
PROP_HSCROLL_POLICY,
PROP_ORIENTATION,
PROP_VADJUSTMENT,
PROP_VSCROLL_POLICY,
N_PROPS
};
/* HACK: We want the g_class argument in our instance init func and G_DEFINE_TYPE() won't let us */
static void gtk_list_base_init_real (GtkListBase *self, GtkListBaseClass *g_class);
#define g_type_register_static_simple(a,b,c,d,e,evil,f) g_type_register_static_simple(a,b,c,d,e, (GInstanceInitFunc) gtk_list_base_init_real, f);
G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GtkListBase, gtk_list_base, GTK_TYPE_WIDGET,
G_ADD_PRIVATE (GtkListBase)
G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
#undef g_type_register_static_simple
G_GNUC_UNUSED static void gtk_list_base_init (GtkListBase *self) { }
static GParamSpec *properties[N_PROPS] = { NULL, };
/*
* gtk_list_base_get_position_from_allocation:
* @self: a `GtkListBase`
* @across: position in pixels in the direction cross to the list
* @along: position in pixels in the direction of the list
* @pos: (out caller-allocates): set to the looked up position
* @area: (out caller-allocates) (optional): set to the area occupied
* by the returned position
*
* Given a coordinate in list coordinates, determine the position of the
* item that occupies that position.
*
* It is possible for @area to not include the point given by (across, along).
* This will happen for example in the last row of a gridview, where the
* last item will be returned for the whole width, even if there are empty
* cells.
*
* Returns: %TRUE on success or %FALSE if no position occupies the given offset.
**/
static guint
gtk_list_base_get_position_from_allocation (GtkListBase *self,
int across,
int along,
guint *pos,
cairo_rectangle_int_t *area)
{
return GTK_LIST_BASE_GET_CLASS (self)->get_position_from_allocation (self, across, along, pos, area);
}
static gboolean
gtk_list_base_adjustment_is_flipped (GtkListBase *self,
GtkOrientation orientation)
{
if (orientation == GTK_ORIENTATION_VERTICAL)
return FALSE;
return gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
}
static void
gtk_list_base_get_adjustment_values (GtkListBase *self,
GtkOrientation orientation,
int *value,
int *size,
int *page_size)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
int val, upper, ps;
val = gtk_adjustment_get_value (priv->adjustment[orientation]);
upper = gtk_adjustment_get_upper (priv->adjustment[orientation]);
ps = gtk_adjustment_get_page_size (priv->adjustment[orientation]);
if (gtk_list_base_adjustment_is_flipped (self, orientation))
val = upper - ps - val;
if (value)
*value = val;
if (size)
*size = upper;
if (page_size)
*page_size = ps;
}
static void
gtk_list_base_adjustment_value_changed_cb (GtkAdjustment *adjustment,
GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
cairo_rectangle_int_t area, cell_area;
int along, across, total_size;
double align_across, align_along;
GtkPackType side_across, side_along;
guint pos;
gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), &area.x, &total_size, &area.width);
if (total_size == area.width)
align_across = 0.5;
else if (adjustment != priv->adjustment[priv->orientation])
align_across = CLAMP (priv->anchor_align_across, 0, 1);
else
align_across = (double) area.x / (total_size - area.width);
across = area.x + round (align_across * area.width);
across = CLAMP (across, 0, total_size - 1);
gtk_list_base_get_adjustment_values (self, priv->orientation, &area.y, &total_size, &area.height);
if (total_size == area.height)
align_along = 0.5;
else if (adjustment != priv->adjustment[OPPOSITE_ORIENTATION(priv->orientation)])
align_along = CLAMP (priv->anchor_align_along, 0, 1);
else
align_along = (double) area.y / (total_size - area.height);
along = area.y + round (align_along * area.height);
along = CLAMP (along, 0, total_size - 1);
if (!gtk_list_base_get_position_from_allocation (self,
across, along,
&pos,
&cell_area))
{
g_warning ("%s failed to scroll to given position. Ignoring...", G_OBJECT_TYPE_NAME (self));
return;
}
/* find an anchor that is in the visible area */
if (cell_area.x < area.x && cell_area.x + cell_area.width <= area.x + area.width)
side_across = GTK_PACK_END;
else if (cell_area.x >= area.x && cell_area.x + cell_area.width > area.x + area.width)
side_across = GTK_PACK_START;
else if (cell_area.x + cell_area.width / 2 > across)
side_across = GTK_PACK_END;
else
side_across = GTK_PACK_START;
if (cell_area.y < area.y && cell_area.y + cell_area.height <= area.y + area.height)
side_along = GTK_PACK_END;
else if (cell_area.y >= area.y && cell_area.y + cell_area.height > area.y + area.height)
side_along = GTK_PACK_START;
else if (cell_area.y + cell_area.height / 2 > along)
side_along = GTK_PACK_END;
else
side_along = GTK_PACK_START;
/* Compute the align based on side to keep the values identical */
if (side_across == GTK_PACK_START)
align_across = (double) (cell_area.x - area.x) / area.width;
else
align_across = (double) (cell_area.x + cell_area.width - area.x) / area.width;
if (side_along == GTK_PACK_START)
align_along = (double) (cell_area.y - area.y) / area.height;
else
align_along = (double) (cell_area.y + cell_area.height - area.y) / area.height;
gtk_list_base_set_anchor (self,
pos,
align_across, side_across,
align_along, side_along);
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
static void
gtk_list_base_clear_adjustment (GtkListBase *self,
GtkOrientation orientation)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->adjustment[orientation] == NULL)
return;
g_signal_handlers_disconnect_by_func (priv->adjustment[orientation],
gtk_list_base_adjustment_value_changed_cb,
self);
g_clear_object (&priv->adjustment[orientation]);
}
/*
* gtk_list_base_move_focus_along:
* @self: a `GtkListBase`
* @pos: position from which to move focus
* @steps: steps to move focus - negative numbers move focus backwards
*
* Moves focus @steps in the direction of the list.
* If focus cannot be moved, @pos is returned.
* If focus should be moved out of the widget, %GTK_INVALID_LIST_POSITION
* is returned.
*
* Returns: new focus position
**/
static guint
gtk_list_base_move_focus_along (GtkListBase *self,
guint pos,
int steps)
{
return GTK_LIST_BASE_GET_CLASS (self)->move_focus_along (self, pos, steps);
}
/*
* gtk_list_base_move_focus_across:
* @self: a `GtkListBase`
* @pos: position from which to move focus
* @steps: steps to move focus - negative numbers move focus backwards
*
* Moves focus @steps in the direction across the list.
* If focus cannot be moved, @pos is returned.
* If focus should be moved out of the widget, %GTK_INVALID_LIST_POSITION
* is returned.
*
* Returns: new focus position
**/
static guint
gtk_list_base_move_focus_across (GtkListBase *self,
guint pos,
int steps)
{
return GTK_LIST_BASE_GET_CLASS (self)->move_focus_across (self, pos, steps);
}
static guint
gtk_list_base_move_focus (GtkListBase *self,
guint pos,
GtkOrientation orientation,
int steps)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (orientation == GTK_ORIENTATION_HORIZONTAL &&
gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
steps = -steps;
if (orientation == priv->orientation)
return gtk_list_base_move_focus_along (self, pos, steps);
else
return gtk_list_base_move_focus_across (self, pos, steps);
}
/*
* gtk_list_base_get_allocation_along:
* @self: a `GtkListBase`
* @pos: item to get the size of
* @offset: (out caller-allocates) (optional): set to the offset
* of the top/left of the item
* @size: (out caller-allocates) (optional): set to the size of
* the item in the direction
*
* Computes the allocation of the item in the direction along the sizing
* axis.
*
* Returns: %TRUE if the item exists and has an allocation, %FALSE otherwise
**/
static gboolean
gtk_list_base_get_allocation_along (GtkListBase *self,
guint pos,
int *offset,
int *size)
{
return GTK_LIST_BASE_GET_CLASS (self)->get_allocation_along (self, pos, offset, size);
}
/*
* gtk_list_base_get_allocation_across:
* @self: a `GtkListBase`
* @pos: item to get the size of
* @offset: (out caller-allocates) (optional): set to the offset
* of the top/left of the item
* @size: (out caller-allocates) (optional): set to the size of
* the item in the direction
*
* Computes the allocation of the item in the direction across to the sizing
* axis.
*
* Returns: %TRUE if the item exists and has an allocation, %FALSE otherwise
**/
static gboolean
gtk_list_base_get_allocation_across (GtkListBase *self,
guint pos,
int *offset,
int *size)
{
return GTK_LIST_BASE_GET_CLASS (self)->get_allocation_across (self, pos, offset, size);
}
/*
* gtk_list_base_select_item:
* @self: a `GtkListBase`
* @pos: item to select
* @modify: %TRUE if the selection should be modified, %FALSE
* if a new selection should be done. This is usually set
* to %TRUE if the user keeps the <Shift> key pressed.
* @extend_pos: %TRUE if the selection should be extended.
* Selections are usually extended from the last selected
* position if the user presses the <Ctrl> key.
*
* Selects the item at @pos according to how GTK list widgets modify
* selections, both when clicking rows with the mouse or when using
* the keyboard.
**/
void
gtk_list_base_select_item (GtkListBase *self,
guint pos,
gboolean modify,
gboolean extend)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkSelectionModel *model;
gboolean success = FALSE;
guint n_items;
model = gtk_list_item_manager_get_model (priv->item_manager);
if (model == NULL)
return;
n_items = g_list_model_get_n_items (G_LIST_MODEL (model));
if (pos >= n_items)
return;
if (extend)
{
guint extend_pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->selected);
if (extend_pos < n_items)
{
guint max = MAX (extend_pos, pos);
guint min = MIN (extend_pos, pos);
if (modify)
{
if (gtk_selection_model_is_selected (model, extend_pos))
{
success = gtk_selection_model_select_range (model,
min,
max - min + 1,
FALSE);
}
else
{
success = gtk_selection_model_unselect_range (model,
min,
max - min + 1);
}
}
else
{
success = gtk_selection_model_select_range (model,
min,
max - min + 1,
TRUE);
}
}
/* If there's no range to select or selecting ranges isn't supported
* by the model, fall through to normal setting.
*/
}
if (success)
return;
if (modify)
{
if (gtk_selection_model_is_selected (model, pos))
gtk_selection_model_unselect_item (model, pos);
else
gtk_selection_model_select_item (model, pos, FALSE);
}
else
{
gtk_selection_model_select_item (model, pos, TRUE);
}
gtk_list_item_tracker_set_position (priv->item_manager,
priv->selected,
pos,
0, 0);
}
guint
gtk_list_base_get_n_items (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->model == NULL)
return 0;
return g_list_model_get_n_items (G_LIST_MODEL (priv->model));
}
guint
gtk_list_base_get_focus_position (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return gtk_list_item_tracker_get_position (priv->item_manager, priv->focus);
}
static gboolean
gtk_list_base_focus (GtkWidget *widget,
GtkDirectionType direction)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
guint old, pos, n_items;
GtkWidget *focus_child;
GtkListItemManagerItem *item;
focus_child = gtk_widget_get_focus_child (widget);
/* focus is moving around fine inside the focus child, don't disturb it */
if (focus_child && gtk_widget_child_focus (focus_child, direction))
return TRUE;
pos = gtk_list_base_get_focus_position (self);
n_items = gtk_list_base_get_n_items (self);
old = pos;
if (pos >= n_items)
{
if (n_items == 0)
return FALSE;
pos = 0;
}
else if (focus_child == NULL)
{
/* Focus was outside the list, just grab the old focus item
* while keeping the selection intact.
*/
old = GTK_INVALID_LIST_POSITION;
}
else
{
switch (direction)
{
case GTK_DIR_TAB_FORWARD:
pos++;
if (pos >= n_items)
return FALSE;
break;
case GTK_DIR_TAB_BACKWARD:
if (pos == 0)
return FALSE;
pos--;
break;
case GTK_DIR_UP:
pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_VERTICAL, -1);
break;
case GTK_DIR_DOWN:
pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_VERTICAL, 1);
break;
case GTK_DIR_LEFT:
pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_HORIZONTAL, -1);
break;
case GTK_DIR_RIGHT:
pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_HORIZONTAL, 1);
break;
default:
g_assert_not_reached ();
return TRUE;
}
}
if (old == pos)
return TRUE;
item = gtk_list_item_manager_get_nth (priv->item_manager, pos, NULL);
if (item == NULL)
return FALSE;
/* This shouldn't really happen, but if it does, oh well */
if (item->widget == NULL)
return gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos, TRUE, FALSE, FALSE);
return gtk_widget_child_focus (item->widget, direction);
}
static gboolean
gtk_list_base_grab_focus (GtkWidget *widget)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
guint pos;
pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->focus);
if (gtk_list_base_grab_focus_on_item (self, pos, FALSE, FALSE, FALSE))
return TRUE;
return GTK_WIDGET_CLASS (gtk_list_base_parent_class)->grab_focus (widget);
}
static void
gtk_list_base_dispose (GObject *object)
{
GtkListBase *self = GTK_LIST_BASE (object);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
gtk_list_base_clear_adjustment (self, GTK_ORIENTATION_HORIZONTAL);
gtk_list_base_clear_adjustment (self, GTK_ORIENTATION_VERTICAL);
if (priv->anchor)
{
gtk_list_item_tracker_free (priv->item_manager, priv->anchor);
priv->anchor = NULL;
}
if (priv->selected)
{
gtk_list_item_tracker_free (priv->item_manager, priv->selected);
priv->selected = NULL;
}
if (priv->focus)
{
gtk_list_item_tracker_free (priv->item_manager, priv->focus);
priv->focus = NULL;
}
g_clear_object (&priv->item_manager);
g_clear_object (&priv->model);
G_OBJECT_CLASS (gtk_list_base_parent_class)->dispose (object);
}
static void
gtk_list_base_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GtkListBase *self = GTK_LIST_BASE (object);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
switch (property_id)
{
case PROP_HADJUSTMENT:
g_value_set_object (value, priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
break;
case PROP_HSCROLL_POLICY:
g_value_set_enum (value, priv->scroll_policy[GTK_ORIENTATION_HORIZONTAL]);
break;
case PROP_ORIENTATION:
g_value_set_enum (value, priv->orientation);
break;
case PROP_VADJUSTMENT:
g_value_set_object (value, priv->adjustment[GTK_ORIENTATION_VERTICAL]);
break;
case PROP_VSCROLL_POLICY:
g_value_set_enum (value, priv->scroll_policy[GTK_ORIENTATION_VERTICAL]);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_list_base_set_adjustment (GtkListBase *self,
GtkOrientation orientation,
GtkAdjustment *adjustment)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->adjustment[orientation] == adjustment)
return;
if (adjustment == NULL)
adjustment = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
g_object_ref_sink (adjustment);
gtk_list_base_clear_adjustment (self, orientation);
priv->adjustment[orientation] = adjustment;
g_signal_connect (adjustment, "value-changed",
G_CALLBACK (gtk_list_base_adjustment_value_changed_cb),
self);
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
static void
gtk_list_base_set_scroll_policy (GtkListBase *self,
GtkOrientation orientation,
GtkScrollablePolicy scroll_policy)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->scroll_policy[orientation] == scroll_policy)
return;
priv->scroll_policy[orientation] = scroll_policy;
gtk_widget_queue_resize (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self),
orientation == GTK_ORIENTATION_HORIZONTAL
? properties[PROP_HSCROLL_POLICY]
: properties[PROP_VSCROLL_POLICY]);
}
static void
gtk_list_base_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GtkListBase *self = GTK_LIST_BASE (object);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
switch (property_id)
{
case PROP_HADJUSTMENT:
gtk_list_base_set_adjustment (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_object (value));
break;
case PROP_HSCROLL_POLICY:
gtk_list_base_set_scroll_policy (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_enum (value));
break;
case PROP_ORIENTATION:
{
GtkOrientation orientation = g_value_get_enum (value);
if (priv->orientation != orientation)
{
priv->orientation = orientation;
gtk_widget_update_orientation (GTK_WIDGET (self), priv->orientation);
gtk_widget_queue_resize (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORIENTATION]);
}
}
break;
case PROP_VADJUSTMENT:
gtk_list_base_set_adjustment (self, GTK_ORIENTATION_VERTICAL, g_value_get_object (value));
break;
case PROP_VSCROLL_POLICY:
gtk_list_base_set_scroll_policy (self, GTK_ORIENTATION_VERTICAL, g_value_get_enum (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gtk_list_base_compute_scroll_align (GtkListBase *self,
GtkOrientation orientation,
int cell_start,
int cell_end,
double current_align,
GtkPackType current_side,
double *new_align,
GtkPackType *new_side)
{
int visible_start, visible_size, visible_end;
int cell_size;
gtk_list_base_get_adjustment_values (GTK_LIST_BASE (self),
orientation,
&visible_start, NULL, &visible_size);
visible_end = visible_start + visible_size;
cell_size = cell_end - cell_start;
if (cell_size <= visible_size)
{
if (cell_start < visible_start)
{
*new_align = 0.0;
*new_side = GTK_PACK_START;
}
else if (cell_end > visible_end)
{
*new_align = 1.0;
*new_side = GTK_PACK_END;
}
else
{
/* XXX: start or end here? */
*new_side = GTK_PACK_START;
*new_align = (double) (cell_start - visible_start) / visible_size;
}
}
else
{
/* This is the unlikely case of the cell being higher than the visible area */
if (cell_start > visible_start)
{
*new_align = 0.0;
*new_side = GTK_PACK_START;
}
else if (cell_end < visible_end)
{
*new_align = 1.0;
*new_side = GTK_PACK_END;
}
else
{
/* the cell already covers the whole screen */
*new_align = current_align;
*new_side = current_side;
}
}
}
static void
gtk_list_base_update_focus_tracker (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkWidget *focus_child;
guint pos;
focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self));
if (!GTK_IS_LIST_ITEM_WIDGET (focus_child))
return;
pos = gtk_list_item_widget_get_position (GTK_LIST_ITEM_WIDGET (focus_child));
if (pos != gtk_list_item_tracker_get_position (priv->item_manager, priv->focus))
{
gtk_list_item_tracker_set_position (priv->item_manager,
priv->focus,
pos,
0,
0);
}
}
static void
gtk_list_base_scroll_to_item (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
int start, end;
double align_along, align_across;
GtkPackType side_along, side_across;
guint pos;
if (!g_variant_check_format_string (parameter, "u", FALSE))
return;
g_variant_get (parameter, "u", &pos);
/* figure out primary orientation and if position is valid */
if (!gtk_list_base_get_allocation_along (GTK_LIST_BASE (self), pos, &start, &end))
return;
end += start;
gtk_list_base_compute_scroll_align (self,
gtk_list_base_get_orientation (GTK_LIST_BASE (self)),
start, end,
priv->anchor_align_along, priv->anchor_side_along,
&align_along, &side_along);
/* now do the same thing with the other orientation */
if (!gtk_list_base_get_allocation_across (GTK_LIST_BASE (self), pos, &start, &end))
return;
end += start;
gtk_list_base_compute_scroll_align (self,
gtk_list_base_get_opposite_orientation (GTK_LIST_BASE (self)),
start, end,
priv->anchor_align_across, priv->anchor_side_across,
&align_across, &side_across);
gtk_list_base_set_anchor (self,
pos,
align_across, side_across,
align_along, side_along);
/* HACK HACK HACK
*
* GTK has no way to track the focused child. But we now that when a listitem
* gets focus, it calls this action. So we update our focus tracker from here
* because it's the closest we can get to accurate tracking.
*/
gtk_list_base_update_focus_tracker (self);
}
static void
gtk_list_base_select_item_action (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkListBase *self = GTK_LIST_BASE (widget);
guint pos;
gboolean modify, extend;
g_variant_get (parameter, "(ubb)", &pos, &modify, &extend);
gtk_list_base_select_item (self, pos, modify, extend);
}
static void
gtk_list_base_select_all (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkSelectionModel *selection_model;
selection_model = gtk_list_item_manager_get_model (priv->item_manager);
if (selection_model == NULL)
return;
gtk_selection_model_select_all (selection_model);
}
static void
gtk_list_base_unselect_all (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkSelectionModel *selection_model;
selection_model = gtk_list_item_manager_get_model (priv->item_manager);
if (selection_model == NULL)
return;
gtk_selection_model_unselect_all (selection_model);
}
static gboolean
gtk_list_base_move_cursor_to_start (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkListBase *self = GTK_LIST_BASE (widget);
gboolean select, modify, extend;
if (gtk_list_base_get_n_items (self) == 0)
return TRUE;
g_variant_get (args, "(bbb)", &select, &modify, &extend);
gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), 0, select, modify, extend);
return TRUE;
}
static gboolean
gtk_list_base_move_cursor_page_up (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
gboolean select, modify, extend;
cairo_rectangle_int_t area, new_area;
int page_size;
guint pos, new_pos;
pos = gtk_list_base_get_focus_position (self);
page_size = gtk_adjustment_get_page_size (priv->adjustment[priv->orientation]);
if (!gtk_list_base_get_allocation_along (self, pos, &area.y, &area.height) ||
!gtk_list_base_get_allocation_across (self, pos, &area.x, &area.width))
return TRUE;
if (!gtk_list_base_get_position_from_allocation (self,
area.x + area.width / 2,
MAX (0, area.y + area.height - page_size),
&new_pos,
&new_area))
return TRUE;
/* We want the whole row to be visible */
if (new_area.y < MAX (0, area.y + area.height - page_size))
new_pos = gtk_list_base_move_focus_along (self, new_pos, 1);
/* But we definitely want to move if we can */
if (new_pos >= pos)
{
new_pos = gtk_list_base_move_focus_along (self, new_pos, -1);
if (new_pos == pos)
return TRUE;
}
g_variant_get (args, "(bbb)", &select, &modify, &extend);
gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), new_pos, select, modify, extend);
return TRUE;
}
static gboolean
gtk_list_base_move_cursor_page_down (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkListBase *self = GTK_LIST_BASE (widget);
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
gboolean select, modify, extend;
cairo_rectangle_int_t area, new_area;
int page_size, end;
guint pos, new_pos;
pos = gtk_list_base_get_focus_position (self);
page_size = gtk_adjustment_get_page_size (priv->adjustment[priv->orientation]);
end = gtk_adjustment_get_upper (priv->adjustment[priv->orientation]);
if (end == 0)
return TRUE;
if (!gtk_list_base_get_allocation_along (self, pos, &area.y, &area.height) ||
!gtk_list_base_get_allocation_across (self, pos, &area.x, &area.width))
return TRUE;
if (!gtk_list_base_get_position_from_allocation (self,
area.x + area.width / 2,
MIN (end, area.y + page_size) - 1,
&new_pos,
&new_area))
return TRUE;
/* We want the whole row to be visible */
if (new_area.y + new_area.height > MIN (end, area.y + page_size))
new_pos = gtk_list_base_move_focus_along (self, new_pos, -1);
/* But we definitely want to move if we can */
if (new_pos <= pos)
{
new_pos = gtk_list_base_move_focus_along (self, new_pos, 1);
if (new_pos == pos)
return TRUE;
}
g_variant_get (args, "(bbb)", &select, &modify, &extend);
gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), new_pos, select, modify, extend);
return TRUE;
}
static gboolean
gtk_list_base_move_cursor_to_end (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkListBase *self = GTK_LIST_BASE (widget);
gboolean select, modify, extend;
guint n_items;
n_items = gtk_list_base_get_n_items (self);
if (n_items == 0)
return TRUE;
g_variant_get (args, "(bbb)", &select, &modify, &extend);
gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), n_items - 1, select, modify, extend);
return TRUE;
}
static gboolean
gtk_list_base_move_cursor (GtkWidget *widget,
GVariant *args,
gpointer unused)
{
GtkListBase *self = GTK_LIST_BASE (widget);
int amount;
guint orientation;
guint pos;
gboolean select, modify, extend;
g_variant_get (args, "(ubbbi)", &orientation, &select, &modify, &extend, &amount);
pos = gtk_list_base_get_focus_position (self);
pos = gtk_list_base_move_focus (self, pos, orientation, amount);
gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos, select, modify, extend);
return TRUE;
}
static void
gtk_list_base_add_move_binding (GtkWidgetClass *widget_class,
guint keyval,
GtkOrientation orientation,
int amount)
{
gtk_widget_class_add_binding (widget_class,
keyval,
0,
gtk_list_base_move_cursor,
"(ubbbi)", orientation, TRUE, FALSE, FALSE, amount);
gtk_widget_class_add_binding (widget_class,
keyval,
GDK_CONTROL_MASK,
gtk_list_base_move_cursor,
"(ubbbi)", orientation, FALSE, FALSE, FALSE, amount);
gtk_widget_class_add_binding (widget_class,
keyval,
GDK_SHIFT_MASK,
gtk_list_base_move_cursor,
"(ubbbi)", orientation, TRUE, FALSE, TRUE, amount);
gtk_widget_class_add_binding (widget_class,
keyval,
GDK_CONTROL_MASK | GDK_SHIFT_MASK,
gtk_list_base_move_cursor,
"(ubbbi)", orientation, TRUE, TRUE, TRUE, amount);
}
static void
gtk_list_base_add_custom_move_binding (GtkWidgetClass *widget_class,
guint keyval,
GtkShortcutFunc callback)
{
gtk_widget_class_add_binding (widget_class,
keyval,
0,
callback,
"(bbb)", TRUE, FALSE, FALSE);
gtk_widget_class_add_binding (widget_class,
keyval,
GDK_CONTROL_MASK,
callback,
"(bbb)", FALSE, FALSE, FALSE);
gtk_widget_class_add_binding (widget_class,
keyval,
GDK_SHIFT_MASK,
callback,
"(bbb)", TRUE, FALSE, TRUE);
gtk_widget_class_add_binding (widget_class,
keyval,
GDK_CONTROL_MASK | GDK_SHIFT_MASK,
callback,
"(bbb)", TRUE, TRUE, TRUE);
}
static void
gtk_list_base_class_init (GtkListBaseClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gpointer iface;
widget_class->focus = gtk_list_base_focus;
widget_class->grab_focus = gtk_list_base_grab_focus;
gobject_class->dispose = gtk_list_base_dispose;
gobject_class->get_property = gtk_list_base_get_property;
gobject_class->set_property = gtk_list_base_set_property;
/* GtkScrollable implementation */
iface = g_type_default_interface_peek (GTK_TYPE_SCROLLABLE);
properties[PROP_HADJUSTMENT] =
g_param_spec_override ("hadjustment",
g_object_interface_find_property (iface, "hadjustment"));
properties[PROP_HSCROLL_POLICY] =
g_param_spec_override ("hscroll-policy",
g_object_interface_find_property (iface, "hscroll-policy"));
properties[PROP_VADJUSTMENT] =
g_param_spec_override ("vadjustment",
g_object_interface_find_property (iface, "vadjustment"));
properties[PROP_VSCROLL_POLICY] =
g_param_spec_override ("vscroll-policy",
g_object_interface_find_property (iface, "vscroll-policy"));
/**
* GtkListBase:orientation:
*
* The orientation of the list. See GtkOrientable:orientation
* for details.
*/
properties[PROP_ORIENTATION] =
g_param_spec_enum ("orientation", NULL, NULL,
GTK_TYPE_ORIENTATION,
GTK_ORIENTATION_VERTICAL,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (gobject_class, N_PROPS, properties);
/**
* GtkListBase|list.scroll-to-item:
* @position: position of item to scroll to
*
* Moves the visible area to the item given in @position with the minimum amount
* of scrolling required. If the item is already visible, nothing happens.
*/
gtk_widget_class_install_action (widget_class,
"list.scroll-to-item",
"u",
gtk_list_base_scroll_to_item);
/**
* GtkListBase|list.select-item:
* @position: position of item to select
* @modify: %TRUE to toggle the existing selection, %FALSE to select
* @extend: %TRUE to extend the selection
*
* Changes selection.
*
* If @extend is %TRUE and the model supports selecting ranges, the
* affected items are all items from the last selected item to the item
* in @position.
* If @extend is %FALSE or selecting ranges is not supported, only the
* item in @position is affected.
*
* If @modify is %TRUE, the affected items will be set to the same state.
* If @modify is %FALSE, the affected items will be selected and
* all other items will be deselected.
*/
gtk_widget_class_install_action (widget_class,
"list.select-item",
"(ubb)",
gtk_list_base_select_item_action);
/**
* GtkListBase|list.select-all:
*
* If the selection model supports it, select all items in the model.
* If not, do nothing.
*/
gtk_widget_class_install_action (widget_class,
"list.select-all",
NULL,
gtk_list_base_select_all);
/**
* GtkListBase|list.unselect-all:
*
* If the selection model supports it, unselect all items in the model.
* If not, do nothing.
*/
gtk_widget_class_install_action (widget_class,
"list.unselect-all",
NULL,
gtk_list_base_unselect_all);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_Up, GTK_ORIENTATION_VERTICAL, -1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Up, GTK_ORIENTATION_VERTICAL, -1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_Down, GTK_ORIENTATION_VERTICAL, 1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Down, GTK_ORIENTATION_VERTICAL, 1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_Left, GTK_ORIENTATION_HORIZONTAL, -1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Left, GTK_ORIENTATION_HORIZONTAL, -1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_Right, GTK_ORIENTATION_HORIZONTAL, 1);
gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Right, GTK_ORIENTATION_HORIZONTAL, 1);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Home, gtk_list_base_move_cursor_to_start);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Home, gtk_list_base_move_cursor_to_start);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_End, gtk_list_base_move_cursor_to_end);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_End, gtk_list_base_move_cursor_to_end);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Page_Up, gtk_list_base_move_cursor_page_up);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Page_Up, gtk_list_base_move_cursor_page_up);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Page_Down, gtk_list_base_move_cursor_page_down);
gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Page_Down, gtk_list_base_move_cursor_page_down);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_a, GDK_CONTROL_MASK, "list.select-all", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_slash, GDK_CONTROL_MASK, "list.select-all", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_A, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "list.unselect-all", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_backslash, GDK_CONTROL_MASK, "list.unselect-all", NULL);
}
static gboolean
autoscroll_cb (GtkWidget *widget,
GdkFrameClock *frame_clock,
gpointer data)
{
GtkListBase *self = data;
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
double value;
double delta_x, delta_y;
value = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
gtk_adjustment_set_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL], value + priv->autoscroll_delta_x);
delta_x = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]) - value;
value = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
gtk_adjustment_set_value (priv->adjustment[GTK_ORIENTATION_VERTICAL], value + priv->autoscroll_delta_y);
delta_y = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]) - value;
if (delta_x != 0 || delta_y != 0)
{
return G_SOURCE_CONTINUE;
}
else
{
priv->autoscroll_id = 0;
return G_SOURCE_REMOVE;
}
}
static void
add_autoscroll (GtkListBase *self,
double delta_x,
double delta_y)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (gtk_list_base_adjustment_is_flipped (self, GTK_ORIENTATION_HORIZONTAL))
priv->autoscroll_delta_x = -delta_x;
else
priv->autoscroll_delta_x = delta_x;
if (gtk_list_base_adjustment_is_flipped (self, GTK_ORIENTATION_VERTICAL))
priv->autoscroll_delta_y = -delta_y;
else
priv->autoscroll_delta_y = delta_y;
if (priv->autoscroll_id == 0)
priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), autoscroll_cb, self, NULL);
}
static void
remove_autoscroll (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->autoscroll_id != 0)
{
gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->autoscroll_id);
priv->autoscroll_id = 0;
}
}
#define SCROLL_EDGE_SIZE 30
static void
update_autoscroll (GtkListBase *self,
double x,
double y)
{
double width, height;
double delta_x, delta_y;
width = gtk_widget_get_width (GTK_WIDGET (self));
if (x < SCROLL_EDGE_SIZE)
delta_x = - (SCROLL_EDGE_SIZE - x)/3.0;
else if (width - x < SCROLL_EDGE_SIZE)
delta_x = (SCROLL_EDGE_SIZE - (width - x))/3.0;
else
delta_x = 0;
if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
delta_x = - delta_x;
height = gtk_widget_get_height (GTK_WIDGET (self));
if (y < SCROLL_EDGE_SIZE)
delta_y = - (SCROLL_EDGE_SIZE - y)/3.0;
else if (height - y < SCROLL_EDGE_SIZE)
delta_y = (SCROLL_EDGE_SIZE - (height - y))/3.0;
else
delta_y = 0;
if (delta_x != 0 || delta_y != 0)
add_autoscroll (self, delta_x, delta_y);
else
remove_autoscroll (self);
}
/**
* gtk_list_base_size_allocate_child:
* @self: The listbase
* @child: The child
* @x: top left coordinate in the across direction
* @y: top right coordinate in the along direction
* @width: size in the across direction
* @height: size in the along direction
*
* Allocates a child widget in the list coordinate system,
* but with the coordinates already offset by the scroll
* offset.
**/
void
gtk_list_base_size_allocate_child (GtkListBase *self,
GtkWidget *child,
int x,
int y,
int width,
int height)
{
GtkAllocation child_allocation;
int self_width, self_height;
self_width = gtk_widget_get_width (GTK_WIDGET (self));
self_height = gtk_widget_get_height (GTK_WIDGET (self));
if (gtk_list_base_get_orientation (GTK_LIST_BASE (self)) == GTK_ORIENTATION_VERTICAL)
{
if (_gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR)
{
child_allocation.x = x;
child_allocation.y = y;
}
else
{
child_allocation.x = self_width - x - width;
child_allocation.y = y;
}
child_allocation.width = width;
child_allocation.height = height;
}
else
{
if (_gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR)
{
child_allocation.x = y;
child_allocation.y = x;
}
else
{
child_allocation.x = self_width - y - height;
child_allocation.y = x;
}
child_allocation.width = height;
child_allocation.height = width;
}
if (!gdk_rectangle_intersect (&child_allocation,
&(GdkRectangle) {
- GTK_LIST_BASE_CHILD_MAX_OVERDRAW,
- GTK_LIST_BASE_CHILD_MAX_OVERDRAW,
self_width + GTK_LIST_BASE_CHILD_MAX_OVERDRAW,
self_height + GTK_LIST_BASE_CHILD_MAX_OVERDRAW
},
NULL))
{
/* child is fully outside the viewport, hide it and don't allocate it */
gtk_widget_set_child_visible (child, FALSE);
return;
}
gtk_widget_set_child_visible (child, TRUE);
gtk_widget_size_allocate (child, &child_allocation, -1);
}
static void
gtk_list_base_widget_to_list (GtkListBase *self,
double x_widget,
double y_widget,
int *across_out,
int *along_out)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkWidget *widget = GTK_WIDGET (self);
if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
x_widget = gtk_widget_get_width (widget) - x_widget;
gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), across_out, NULL, NULL);
gtk_list_base_get_adjustment_values (self, priv->orientation, along_out, NULL, NULL);
if (priv->orientation == GTK_ORIENTATION_VERTICAL)
{
*across_out += x_widget;
*along_out += y_widget;
}
else
{
*across_out += y_widget;
*along_out += x_widget;
}
}
static GtkBitset *
gtk_list_base_get_items_in_rect (GtkListBase *self,
const GdkRectangle *rect)
{
return GTK_LIST_BASE_GET_CLASS (self)->get_items_in_rect (self, rect);
}
static gboolean
gtk_list_base_get_rubberband_coords (GtkListBase *self,
GdkRectangle *rect)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
int x1, x2, y1, y2;
if (!priv->rubberband)
return FALSE;
if (priv->rubberband->start_tracker == NULL)
{
x1 = 0;
y1 = 0;
}
else
{
guint pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->rubberband->start_tracker);
if (gtk_list_base_get_allocation_along (self, pos, &y1, &y2) &&
gtk_list_base_get_allocation_across (self, pos, &x1, &x2))
{
x1 += x2 * priv->rubberband->start_align_across;
y1 += y2 * priv->rubberband->start_align_along;
}
else
{
x1 = 0;
y1 = 0;
}
}
gtk_list_base_widget_to_list (self,
priv->rubberband->pointer_x, priv->rubberband->pointer_y,
&x2, &y2);
rect->x = MIN (x1, x2);
rect->y = MIN (y1, y2);
rect->width = ABS (x1 - x2) + 1;
rect->height = ABS (y1 - y2) + 1;
return TRUE;
}
void
gtk_list_base_allocate_rubberband (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkRequisition min_size;
GdkRectangle rect;
int offset_x, offset_y;
if (!gtk_list_base_get_rubberband_coords (self, &rect))
return;
gtk_widget_get_preferred_size (priv->rubberband->widget, &min_size, NULL);
rect.width = MAX (min_size.width, rect.width);
rect.height = MAX (min_size.height, rect.height);
gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), &offset_x, NULL, NULL);
gtk_list_base_get_adjustment_values (self, priv->orientation, &offset_y, NULL, NULL);
rect.x -= offset_x;
rect.y -= offset_y;
gtk_list_base_size_allocate_child (self,
priv->rubberband->widget,
rect.x, rect.y, rect.width, rect.height);
}
static void
gtk_list_base_start_rubberband (GtkListBase *self,
double x,
double y)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
cairo_rectangle_int_t item_area;
int list_x, list_y;
guint pos;
if (priv->rubberband)
return;
gtk_list_base_widget_to_list (self, x, y, &list_x, &list_y);
if (!gtk_list_base_get_position_from_allocation (self, list_x, list_y, &pos, &item_area))
{
g_warning ("Could not start rubberbanding: No item\n");
return;
}
priv->rubberband = g_new0 (RubberbandData, 1);
priv->rubberband->start_tracker = gtk_list_item_tracker_new (priv->item_manager);
gtk_list_item_tracker_set_position (priv->item_manager, priv->rubberband->start_tracker, pos, 0, 0);
priv->rubberband->start_align_across = (double) (list_x - item_area.x) / item_area.width;
priv->rubberband->start_align_along = (double) (list_y - item_area.y) / item_area.height;
priv->rubberband->pointer_x = x;
priv->rubberband->pointer_y = y;
priv->rubberband->widget = gtk_gizmo_new ("rubberband",
NULL, NULL, NULL, NULL, NULL, NULL);
gtk_widget_set_parent (priv->rubberband->widget, GTK_WIDGET (self));
}
static void
gtk_list_base_stop_rubberband (GtkListBase *self,
gboolean modify,
gboolean extend)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkListItemManagerItem *item;
GtkSelectionModel *model;
if (!priv->rubberband)
return;
for (item = gtk_list_item_manager_get_first (priv->item_manager);
item != NULL;
item = gtk_rb_tree_node_get_next (item))
{
if (item->widget)
gtk_widget_unset_state_flags (item->widget, GTK_STATE_FLAG_ACTIVE);
}
model = gtk_list_item_manager_get_model (priv->item_manager);
if (model != NULL)
{
GtkBitset *selected, *mask;
GdkRectangle rect;
GtkBitset *rubberband_selection;
if (!gtk_list_base_get_rubberband_coords (self, &rect))
return;
rubberband_selection = gtk_list_base_get_items_in_rect (self, &rect);
if (modify && extend) /* Ctrl + Shift */
{
if (gtk_bitset_is_empty (rubberband_selection))
{
selected = gtk_bitset_ref (rubberband_selection);
mask = gtk_bitset_ref (rubberband_selection);
}
else
{
GtkBitset *current;
guint min = gtk_bitset_get_minimum (rubberband_selection);
guint max = gtk_bitset_get_maximum (rubberband_selection);
/* toggle the rubberband, keep the rest */
current = gtk_selection_model_get_selection_in_range (model, min, max - min + 1);
selected = gtk_bitset_copy (current);
gtk_bitset_unref (current);
gtk_bitset_intersect (selected, rubberband_selection);
gtk_bitset_difference (selected, rubberband_selection);
mask = gtk_bitset_ref (rubberband_selection);
}
}
else if (modify) /* Ctrl */
{
/* select the rubberband, keep the rest */
selected = gtk_bitset_ref (rubberband_selection);
mask = gtk_bitset_ref (rubberband_selection);
}
else if (extend) /* Shift */
{
/* unselect the rubberband, keep the rest */
selected = gtk_bitset_new_empty ();
mask = gtk_bitset_ref (rubberband_selection);
}
else /* no modifier */
{
/* select the rubberband, clear the rest */
selected = gtk_bitset_ref (rubberband_selection);
mask = gtk_bitset_new_empty ();
gtk_bitset_add_range (mask, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
}
gtk_selection_model_set_selection (model, selected, mask);
gtk_bitset_unref (selected);
gtk_bitset_unref (mask);
gtk_bitset_unref (rubberband_selection);
}
gtk_list_item_tracker_free (priv->item_manager, priv->rubberband->start_tracker);
g_clear_pointer (&priv->rubberband->widget, gtk_widget_unparent);
g_free (priv->rubberband);
priv->rubberband = NULL;
remove_autoscroll (self);
}
static void
gtk_list_base_update_rubberband_selection (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkListItemManagerItem *item;
GdkRectangle rect;
guint pos;
GtkBitset *rubberband_selection;
if (!gtk_list_base_get_rubberband_coords (self, &rect))
return;
rubberband_selection = gtk_list_base_get_items_in_rect (self, &rect);
pos = 0;
for (item = gtk_list_item_manager_get_first (priv->item_manager);
item != NULL;
item = gtk_rb_tree_node_get_next (item))
{
if (item->widget)
{
if (gtk_bitset_contains (rubberband_selection, pos))
gtk_widget_set_state_flags (item->widget, GTK_STATE_FLAG_ACTIVE, FALSE);
else
gtk_widget_unset_state_flags (item->widget, GTK_STATE_FLAG_ACTIVE);
}
pos += item->n_items;
}
gtk_bitset_unref (rubberband_selection);
}
static void
gtk_list_base_update_rubberband (GtkListBase *self,
double x,
double y)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (!priv->rubberband)
return;
priv->rubberband->pointer_x = x;
priv->rubberband->pointer_y = y;
gtk_list_base_update_rubberband_selection (self);
update_autoscroll (self, x, y);
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
static void
get_selection_modifiers (GtkGesture *gesture,
gboolean *modify,
gboolean *extend)
{
GdkEventSequence *sequence;
GdkEvent *event;
GdkModifierType state;
*modify = FALSE;
*extend = FALSE;
sequence = gtk_gesture_get_last_updated_sequence (gesture);
event = gtk_gesture_get_last_event (gesture, sequence);
state = gdk_event_get_modifier_state (event);
if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK)
*modify = TRUE;
if ((state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK)
*extend = TRUE;
}
static void
gtk_list_base_drag_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
double start_x, start_y;
gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
if (!priv->rubberband)
{
if (!gtk_drag_check_threshold_double (GTK_WIDGET (self), 0, 0, offset_x, offset_y))
return;
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
gtk_list_base_start_rubberband (self, start_x, start_y);
}
gtk_list_base_update_rubberband (self, start_x + offset_x, start_y + offset_y);
}
static void
gtk_list_base_drag_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
gboolean modify, extend;
if (!priv->rubberband)
return;
gtk_list_base_drag_update (gesture, offset_x, offset_y, self);
get_selection_modifiers (GTK_GESTURE (gesture), &modify, &extend);
gtk_list_base_stop_rubberband (self, modify, extend);
}
void
gtk_list_base_set_enable_rubberband (GtkListBase *self,
gboolean enable)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->enable_rubberband == enable)
return;
priv->enable_rubberband = enable;
if (enable)
{
priv->drag_gesture = gtk_gesture_drag_new ();
g_signal_connect (priv->drag_gesture, "drag-update", G_CALLBACK (gtk_list_base_drag_update), self);
g_signal_connect (priv->drag_gesture, "drag-end", G_CALLBACK (gtk_list_base_drag_end), self);
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
}
else
{
gtk_widget_remove_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
priv->drag_gesture = NULL;
}
}
gboolean
gtk_list_base_get_enable_rubberband (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return priv->enable_rubberband;
}
static void
gtk_list_base_drag_motion (GtkDropControllerMotion *motion,
double x,
double y,
gpointer unused)
{
GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion));
update_autoscroll (GTK_LIST_BASE (widget), x, y);
}
static void
gtk_list_base_drag_leave (GtkDropControllerMotion *motion,
gpointer unused)
{
GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion));
remove_autoscroll (GTK_LIST_BASE (widget));
}
static void
gtk_list_base_init_real (GtkListBase *self,
GtkListBaseClass *g_class)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkEventController *controller;
priv->item_manager = gtk_list_item_manager_new_for_size (GTK_WIDGET (self),
g_class->list_item_name,
g_class->list_item_role,
g_class->list_item_size,
g_class->list_item_augment_size,
g_class->list_item_augment_func);
priv->anchor = gtk_list_item_tracker_new (priv->item_manager);
priv->anchor_side_along = GTK_PACK_START;
priv->anchor_side_across = GTK_PACK_START;
priv->selected = gtk_list_item_tracker_new (priv->item_manager);
priv->focus = gtk_list_item_tracker_new (priv->item_manager);
priv->adjustment[GTK_ORIENTATION_HORIZONTAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
g_object_ref_sink (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
priv->adjustment[GTK_ORIENTATION_VERTICAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
g_object_ref_sink (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
priv->orientation = GTK_ORIENTATION_VERTICAL;
gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
controller = gtk_drop_controller_motion_new ();
g_signal_connect (controller, "motion", G_CALLBACK (gtk_list_base_drag_motion), NULL);
g_signal_connect (controller, "leave", G_CALLBACK (gtk_list_base_drag_leave), NULL);
gtk_widget_add_controller (GTK_WIDGET (self), controller);
}
static int
gtk_list_base_set_adjustment_values (GtkListBase *self,
GtkOrientation orientation,
int value,
int size,
int page_size)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
size = MAX (size, page_size);
value = MAX (value, 0);
value = MIN (value, size - page_size);
g_signal_handlers_block_by_func (priv->adjustment[orientation],
gtk_list_base_adjustment_value_changed_cb,
self);
gtk_adjustment_configure (priv->adjustment[orientation],
gtk_list_base_adjustment_is_flipped (self, orientation)
? size - page_size - value
: value,
0,
size,
page_size * 0.1,
page_size * 0.9,
page_size);
g_signal_handlers_unblock_by_func (priv->adjustment[orientation],
gtk_list_base_adjustment_value_changed_cb,
self);
return value;
}
void
gtk_list_base_update_adjustments (GtkListBase *self,
int total_across,
int total_along,
int page_across,
int page_along,
int *across,
int *along)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
int value_along, value_across, size;
guint pos;
pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->anchor);
if (pos == GTK_INVALID_LIST_POSITION)
{
value_across = 0;
value_along = 0;
}
else
{
if (gtk_list_base_get_allocation_across (self, pos, &value_across, &size))
{
if (priv->anchor_side_across == GTK_PACK_END)
value_across += size;
value_across -= priv->anchor_align_across * page_across;
}
else
{
value_along = 0;
}
if (gtk_list_base_get_allocation_along (self, pos, &value_along, &size))
{
if (priv->anchor_side_along == GTK_PACK_END)
value_along += size;
value_along -= priv->anchor_align_along * page_along;
}
else
{
value_along = 0;
}
}
*across = gtk_list_base_set_adjustment_values (self,
OPPOSITE_ORIENTATION (priv->orientation),
value_across,
total_across,
page_across);
*along = gtk_list_base_set_adjustment_values (self,
priv->orientation,
value_along,
total_along,
page_along);
}
GtkScrollablePolicy
gtk_list_base_get_scroll_policy (GtkListBase *self,
GtkOrientation orientation)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return priv->scroll_policy[orientation];
}
GtkOrientation
gtk_list_base_get_orientation (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return priv->orientation;
}
GtkListItemManager *
gtk_list_base_get_manager (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return priv->item_manager;
}
guint
gtk_list_base_get_anchor (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return gtk_list_item_tracker_get_position (priv->item_manager,
priv->anchor);
}
/*
* gtk_list_base_set_anchor:
* @self: a `GtkListBase`
* @anchor_pos: position of the item to anchor
* @anchor_align_across: how far in the across direction to anchor
* @anchor_side_across: if the anchor should side to start or end of item
* @anchor_align_along: how far in the along direction to anchor
* @anchor_side_along: if the anchor should side to start or end of item
*
* Sets the anchor.
* The anchor is the item that is always kept on screen.
*
* In each dimension, anchoring uses 2 variables: The side of the
* item that gets anchored - either start or end - and where in
* the widget's allocation it should get anchored - here 0.0 means
* the start of the widget and 1.0 is the end of the widget.
* It is allowed to use values outside of this range. In particular,
* this is necessary when the items are larger than the list's
* allocation.
*
* Using this information, the adjustment's value and in turn widget
* offsets will then be computed. If the anchor is too far off, it
* will be clamped so that there are always visible items on screen.
*
* Making anchoring this complicated ensures that one item - one
* corner of one item to be exact - always stays at the same place
* (usually this item is the focused item). So when the list undergoes
* heavy changes (like sorting, filtering, removals, additions), this
* item will stay in place while everything around it will shuffle
* around.
*
* The anchor will also ensure that enough widgets are created according
* to gtk_list_base_set_anchor_max_widgets().
**/
void
gtk_list_base_set_anchor (GtkListBase *self,
guint anchor_pos,
double anchor_align_across,
GtkPackType anchor_side_across,
double anchor_align_along,
GtkPackType anchor_side_along)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
guint items_before;
items_before = round (priv->center_widgets * CLAMP (anchor_align_along, 0, 1));
gtk_list_item_tracker_set_position (priv->item_manager,
priv->anchor,
anchor_pos,
items_before + priv->above_below_widgets,
priv->center_widgets - items_before + priv->above_below_widgets);
priv->anchor_align_across = anchor_align_across;
priv->anchor_side_across = anchor_side_across;
priv->anchor_align_along = anchor_align_along;
priv->anchor_side_along = anchor_side_along;
gtk_widget_queue_allocate (GTK_WIDGET (self));
}
/**
* gtk_list_base_set_anchor_max_widgets:
* @self: a `GtkListBase`
* @center: the number of widgets in the middle
* @above_below: extra widgets above and below
*
* Sets how many widgets should be kept alive around the anchor.
* The number of these widgets determines how many items can be
* displayed and must be chosen to be large enough to cover the
* allocation but should be kept as small as possible for
* performance reasons.
*
* There will be @center widgets allocated around the anchor
* evenly distributed according to the anchor's alignment - if
* the anchor is at the start, all these widgets will be allocated
* behind it, if it's at the end, all the widgets will be allocated
* in front of it.
*
* Addditionally, there will be @above_below widgets allocated both
* before and after the sencter widgets, so the total number of
* widgets kept alive is 2 * above_below + center + 1.
**/
void
gtk_list_base_set_anchor_max_widgets (GtkListBase *self,
guint n_center,
guint n_above_below)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
priv->center_widgets = n_center;
priv->above_below_widgets = n_above_below;
gtk_list_base_set_anchor (self,
gtk_list_item_tracker_get_position (priv->item_manager, priv->anchor),
priv->anchor_align_across,
priv->anchor_side_across,
priv->anchor_align_along,
priv->anchor_side_along);
}
/*
* gtk_list_base_grab_focus_on_item:
* @self: a `GtkListBase`
* @pos: position of the item to focus
* @select: %TRUE to select the item
* @modify: if selecting, %TRUE to modify the selected
* state, %FALSE to always select
* @extend: if selecting, %TRUE to extend the selection,
* %FALSE to only operate on this item
*
* Tries to grab focus on the given item. If there is no item
* at this position or grabbing focus failed, %FALSE will be
* returned.
*
* Returns: %TRUE if focusing the item succeeded
**/
gboolean
gtk_list_base_grab_focus_on_item (GtkListBase *self,
guint pos,
gboolean select,
gboolean modify,
gboolean extend)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
GtkListItemManagerItem *item;
gboolean success;
item = gtk_list_item_manager_get_nth (priv->item_manager, pos, NULL);
if (item == NULL)
return FALSE;
if (!item->widget)
{
GtkListItemTracker *tracker = gtk_list_item_tracker_new (priv->item_manager);
/* We need a tracker here to create the widget.
* That needs to have happened or we can't grab it.
* And we can't use a different tracker, because they manage important rows,
* so we create a temporary one. */
gtk_list_item_tracker_set_position (priv->item_manager, tracker, pos, 0, 0);
item = gtk_list_item_manager_get_nth (priv->item_manager, pos, NULL);
g_assert (item->widget);
success = gtk_widget_grab_focus (item->widget);
gtk_list_item_tracker_free (priv->item_manager, tracker);
}
else
{
success = gtk_widget_grab_focus (item->widget);
}
if (!success)
return FALSE;
if (select)
gtk_list_base_select_item (self, pos, modify, extend);
return TRUE;
}
GtkSelectionModel *
gtk_list_base_get_model (GtkListBase *self)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
return priv->model;
}
gboolean
gtk_list_base_set_model (GtkListBase *self,
GtkSelectionModel *model)
{
GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
if (priv->model == model)
return FALSE;
g_clear_object (&priv->model);
if (model)
{
priv->model = g_object_ref (model);
gtk_list_item_manager_set_model (priv->item_manager, model);
gtk_list_base_set_anchor (self, 0, 0.0, GTK_PACK_START, 0.0, GTK_PACK_START);
}
else
{
gtk_list_item_manager_set_model (priv->item_manager, NULL);
}
return TRUE;
}