forked from AuroraMiddleware/gtk
f63e6394ac
If multiple nested widgets have drag sources on them, both using bubble phase, we need to reliably pick the inner one. Both of them will try to start dragging, and we need to make sure there are no situations where the outer widget starts drag earlier and cancels the inner one. Currently, this can easily happen via integer rounding: start and current coordinates passed into gtk_drag_check_threshold() are initially doubles (other than in GtkNotebook and GtkIconView), and are casted to ints. Then those rounded values are used to calculate deltas to compare to the drag threshold, losing quite a lot of precision along the way, and often resulting in the outer widget getting larger deltas. To avoid it, just don't round it. Introduce a variant of the function that operates on doubles: gtk_drag_check_threshold_double() and use it instead of the original everywhere.
2137 lines
72 KiB
C
2137 lines
72 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 "gtkintl.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"
|
|
|
|
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) (allow-none): 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) (allow-none) set to the offset
|
|
* of the top/left of the item
|
|
* @size: (out caller-allocates) (allow-none) 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) (allow-none) set to the offset
|
|
* of the top/left of the item
|
|
* @size: (out caller-allocates) (allow-none) 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))
|
|
success = gtk_selection_model_unselect_item (model, pos);
|
|
else
|
|
success = gtk_selection_model_select_item (model, pos, FALSE);
|
|
}
|
|
else
|
|
{
|
|
success = 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 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;
|
|
|
|
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",
|
|
P_("Orientation"),
|
|
P_("The orientation of the orientable"),
|
|
GTK_TYPE_ORIENTATION,
|
|
GTK_ORIENTATION_VERTICAL,
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | 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;
|
|
|
|
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;
|
|
child_allocation.width = width;
|
|
child_allocation.height = height;
|
|
}
|
|
else
|
|
{
|
|
int mirror_point = gtk_widget_get_width (GTK_WIDGET (self));
|
|
|
|
child_allocation.x = mirror_point - 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;
|
|
child_allocation.width = height;
|
|
child_allocation.height = width;
|
|
}
|
|
else
|
|
{
|
|
int mirror_point = gtk_widget_get_width (GTK_WIDGET (self));
|
|
|
|
child_allocation.x = mirror_point - y - height;
|
|
child_allocation.y = x;
|
|
child_allocation.width = height;
|
|
child_allocation.height = width;
|
|
}
|
|
}
|
|
|
|
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 (gtk_bitset_is_empty (rubberband_selection))
|
|
{
|
|
gtk_bitset_unref (rubberband_selection);
|
|
return;
|
|
}
|
|
|
|
if (modify && extend) /* Ctrl + Shift */
|
|
{
|
|
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)
|
|
{
|
|
gboolean modify, extend;
|
|
|
|
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);
|
|
priv->adjustment[GTK_ORIENTATION_VERTICAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
|
|
|
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;
|
|
}
|