gem-graph-client/libide/sourceview/ide-completion-list-box.c

939 lines
28 KiB
C

/* ide-completion-list-box.c
*
* Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "ide-completion-list-box"
#include "config.h"
#include "ide-completion-context.h"
#include "ide-completion-list-box.h"
#include "ide-completion-list-box-row.h"
#include "ide-completion-private.h"
#include "ide-completion-proposal.h"
#include "ide-completion-provider.h"
struct _IdeCompletionListBox
{
DzlBin parent_instance;
/* The box containing the rows. */
GtkBox *box;
/* The event box for button press events */
GtkEventBox *events;
/* Font stylign for rows */
PangoAttrList *font_attrs;
/*
* The completion context that is being displayed.
*/
IdeCompletionContext *context;
/*
* The handler for IdeCompletionContecxt::items-chaged which should
* be disconnected when no longer needed.
*/
gulong items_changed_handler;
/*
* The number of rows we expect to have visible to the user.
*/
guint n_rows;
/*
* The currently selected index within the result set. Signed to
* ensure our math in various places allows going negative to catch
* lower edge.
*/
gint selected;
/*
* This is set whenever we make a change that requires updating the
* row content. We delay the update until the next frame callback so
* that we only update once right before we draw the frame. This helps
* reduce duplicate work when reacting to ::items-changed in the model.
*/
guint queued_update;
/*
* These size groups are used to keep each portion of the proposal row
* aligned with each other. Since we only have a fixed number of visible
* rows, the overhead here is negligable by introducing the size cycle.
*/
GtkSizeGroup *left_size_group;
GtkSizeGroup *center_size_group;
GtkSizeGroup *right_size_group;
/*
* The adjustments for scrolling the GtkScrollable.
*/
GtkAdjustment *hadjustment;
GtkAdjustment *vadjustment;
/*
* Gesture to handle button press/touch events.
*/
GtkGesture *multipress_gesture;
};
typedef struct
{
IdeCompletionListBox *self;
IdeCompletionContext *context;
guint n_items;
guint position;
guint selected;
} UpdateState;
enum {
PROP_0,
PROP_CONTEXT,
PROP_PROPOSAL,
PROP_N_ROWS,
PROP_HADJUSTMENT,
PROP_HSCROLL_POLICY,
PROP_VADJUSTMENT,
PROP_VSCROLL_POLICY,
N_PROPS
};
enum {
REPOSITION,
N_SIGNALS
};
static void ide_completion_list_box_queue_update (IdeCompletionListBox *self);
G_DEFINE_FINAL_TYPE_WITH_CODE (IdeCompletionListBox, ide_completion_list_box, DZL_TYPE_BIN,
G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];
static guint
ide_completion_list_box_get_offset (IdeCompletionListBox *self)
{
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
return gtk_adjustment_get_value (self->vadjustment);
}
static void
ide_completion_list_box_set_offset (IdeCompletionListBox *self,
guint offset)
{
gdouble value = offset;
gdouble page_size;
gdouble upper;
gdouble lower;
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
lower = gtk_adjustment_get_lower (self->vadjustment);
upper = gtk_adjustment_get_upper (self->vadjustment);
page_size = gtk_adjustment_get_page_size (self->vadjustment);
if (value > (upper - page_size))
value = upper - page_size;
if (value < lower)
value = lower;
gtk_adjustment_set_value (self->vadjustment, value);
}
static void
ide_completion_list_box_value_changed (IdeCompletionListBox *self,
GtkAdjustment *vadj)
{
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (GTK_IS_ADJUSTMENT (vadj));
ide_completion_list_box_queue_update (self);
}
static void
ide_completion_list_box_set_hadjustment (IdeCompletionListBox *self,
GtkAdjustment *hadjustment)
{
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (!hadjustment || GTK_IS_ADJUSTMENT (hadjustment));
if (g_set_object (&self->hadjustment, hadjustment))
ide_completion_list_box_queue_update (self);
}
static void
ide_completion_list_box_set_vadjustment (IdeCompletionListBox *self,
GtkAdjustment *vadjustment)
{
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (!vadjustment || GTK_IS_ADJUSTMENT (vadjustment));
if (self->vadjustment == vadjustment)
return;
if (self->vadjustment)
{
g_signal_handlers_disconnect_by_func (self->vadjustment,
G_CALLBACK (ide_completion_list_box_value_changed),
self);
g_clear_object (&self->vadjustment);
}
if (vadjustment)
{
self->vadjustment = g_object_ref (vadjustment);
gtk_adjustment_set_lower (self->vadjustment, 0);
gtk_adjustment_set_upper (self->vadjustment, 0);
gtk_adjustment_set_value (self->vadjustment, 0);
gtk_adjustment_set_step_increment (self->vadjustment, 1);
gtk_adjustment_set_page_size (self->vadjustment, self->n_rows);
gtk_adjustment_set_page_increment (self->vadjustment, self->n_rows);
g_signal_connect_object (self->vadjustment,
"value-changed",
G_CALLBACK (ide_completion_list_box_value_changed),
self,
G_CONNECT_SWAPPED);
}
ide_completion_list_box_queue_update (self);
}
static void
ide_completion_list_box_add (GtkContainer *container,
GtkWidget *widget)
{
IdeCompletionListBox *self = (IdeCompletionListBox *)container;
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (GTK_IS_WIDGET (widget));
if (IDE_IS_COMPLETION_LIST_BOX_ROW (widget))
gtk_container_add (GTK_CONTAINER (self->box), widget);
else
GTK_CONTAINER_CLASS (ide_completion_list_box_parent_class)->add (container, widget);
}
static guint
get_row_at_y (IdeCompletionListBox *self,
gdouble y)
{
GtkAllocation alloc;
guint offset;
guint n_items;
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (G_IS_LIST_MODEL (self->context));
gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
offset = ide_completion_list_box_get_offset (self);
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context));
n_items = MAX (1, MIN (self->n_rows, n_items));
return offset + (y / (alloc.height / n_items));
}
static void
multipress_gesture_pressed (GtkGestureMultiPress *gesture,
guint n_press,
gdouble x,
gdouble y,
IdeCompletionListBox *self)
{
g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
if (self->context == NULL)
return;
self->selected = get_row_at_y (self, y);
ide_completion_list_box_queue_update (self);
}
static void
multipress_gesture_released (GtkGestureMultiPress *gesture,
guint n_press,
gdouble x,
gdouble y,
IdeCompletionListBoxRow *self)
{
g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
}
static void
ide_completion_list_box_constructed (GObject *object)
{
IdeCompletionListBox *self = (IdeCompletionListBox *)object;
G_OBJECT_CLASS (ide_completion_list_box_parent_class)->constructed (object);
if (self->hadjustment == NULL)
self->hadjustment = gtk_adjustment_new (0, 0, 0, 0, 0, 0);
if (self->vadjustment == NULL)
self->vadjustment = gtk_adjustment_new (0, 0, 0, 0, 0, 0);
gtk_adjustment_set_lower (self->hadjustment, 0);
gtk_adjustment_set_upper (self->hadjustment, 0);
gtk_adjustment_set_value (self->hadjustment, 0);
ide_completion_list_box_queue_update (self);
}
static void
ide_completion_list_box_finalize (GObject *object)
{
IdeCompletionListBox *self = (IdeCompletionListBox *)object;
g_clear_object (&self->multipress_gesture);
g_clear_object (&self->left_size_group);
g_clear_object (&self->center_size_group);
g_clear_object (&self->right_size_group);
g_clear_object (&self->hadjustment);
g_clear_object (&self->vadjustment);
g_clear_pointer (&self->font_attrs, pango_attr_list_unref);
G_OBJECT_CLASS (ide_completion_list_box_parent_class)->finalize (object);
}
static void
ide_completion_list_box_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
IdeCompletionListBox *self = IDE_COMPLETION_LIST_BOX (object);
switch (prop_id)
{
case PROP_CONTEXT:
g_value_set_object (value, ide_completion_list_box_get_context (self));
break;
case PROP_PROPOSAL:
g_value_take_object (value, ide_completion_list_box_get_proposal (self));
break;
case PROP_N_ROWS:
g_value_set_uint (value, ide_completion_list_box_get_n_rows (self));
break;
case PROP_HADJUSTMENT:
g_value_set_object (value, self->hadjustment);
break;
case PROP_VADJUSTMENT:
g_value_set_object (value, self->vadjustment);
break;
case PROP_HSCROLL_POLICY:
g_value_set_enum (value, GTK_SCROLL_NATURAL);
break;
case PROP_VSCROLL_POLICY:
g_value_set_enum (value, GTK_SCROLL_NATURAL);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_completion_list_box_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
IdeCompletionListBox *self = IDE_COMPLETION_LIST_BOX (object);
switch (prop_id)
{
case PROP_CONTEXT:
ide_completion_list_box_set_context (self, g_value_get_object (value));
break;
case PROP_N_ROWS:
ide_completion_list_box_set_n_rows (self, g_value_get_uint (value));
break;
case PROP_HADJUSTMENT:
ide_completion_list_box_set_hadjustment (self, g_value_get_object (value));
break;
case PROP_VADJUSTMENT:
ide_completion_list_box_set_vadjustment (self, g_value_get_object (value));
break;
case PROP_HSCROLL_POLICY:
/* Do nothing */
break;
case PROP_VSCROLL_POLICY:
/* Do nothing */
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ide_completion_list_box_class_init (IdeCompletionListBoxClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
object_class->constructed = ide_completion_list_box_constructed;
object_class->finalize = ide_completion_list_box_finalize;
object_class->get_property = ide_completion_list_box_get_property;
object_class->set_property = ide_completion_list_box_set_property;
container_class->add = ide_completion_list_box_add;
properties [PROP_CONTEXT] =
g_param_spec_object ("context",
"Context",
"The context being displayed",
IDE_TYPE_COMPLETION_CONTEXT,
(G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
properties [PROP_HADJUSTMENT] =
g_param_spec_object ("hadjustment", NULL, NULL,
GTK_TYPE_ADJUSTMENT,
(G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
properties [PROP_HSCROLL_POLICY] =
g_param_spec_enum ("hscroll-policy", NULL, NULL,
GTK_TYPE_SCROLLABLE_POLICY,
GTK_SCROLL_NATURAL,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
properties [PROP_VADJUSTMENT] =
g_param_spec_object ("vadjustment", NULL, NULL,
GTK_TYPE_ADJUSTMENT,
(G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
properties [PROP_VSCROLL_POLICY] =
g_param_spec_enum ("vscroll-policy", NULL, NULL,
GTK_TYPE_SCROLLABLE_POLICY,
GTK_SCROLL_NATURAL,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
properties [PROP_PROPOSAL] =
g_param_spec_object ("proposal",
"Proposal",
"The proposal that is currently selected",
IDE_TYPE_COMPLETION_PROPOSAL,
(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
properties [PROP_N_ROWS] =
g_param_spec_uint ("n-rows",
"N Rows",
"The number of visible rows",
1, 32, 5,
(G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPS, properties);
signals [REPOSITION] =
g_signal_new_class_handler ("reposition",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
NULL, NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
g_signal_set_va_marshaller (signals [REPOSITION],
G_TYPE_FROM_CLASS (klass),
g_cclosure_marshal_VOID__VOIDv);
gtk_widget_class_set_css_name (widget_class, "list");
}
static void
ide_completion_list_box_init (IdeCompletionListBox *self)
{
self->events = g_object_new (GTK_TYPE_EVENT_BOX,
"visible", TRUE,
NULL);
gtk_widget_add_events (GTK_WIDGET (self->events), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
g_signal_connect (self->events,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&self->events);
gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->events));
self->box = g_object_new (GTK_TYPE_BOX,
"orientation", GTK_ORIENTATION_VERTICAL,
"visible", TRUE,
NULL);
g_signal_connect (self->box,
"destroy",
G_CALLBACK (gtk_widget_destroyed),
&self->box);
gtk_container_add (GTK_CONTAINER (self->events), GTK_WIDGET (self->box));
self->left_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
self->center_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
self->right_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
self->multipress_gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self->events));
gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (self->multipress_gesture), GTK_PHASE_BUBBLE);
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->multipress_gesture), FALSE);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->multipress_gesture), GDK_BUTTON_PRIMARY);
g_signal_connect_object (self->multipress_gesture, "pressed",
G_CALLBACK (multipress_gesture_pressed), self, 0);
g_signal_connect_object (self->multipress_gesture, "released",
G_CALLBACK (multipress_gesture_released), self, 0);
}
static void
ide_completion_list_box_update_row_cb (GtkWidget *widget,
gpointer user_data)
{
g_autoptr(IdeCompletionProposal) proposal = NULL;
g_autoptr(IdeCompletionProvider) provider = NULL;
UpdateState *state = user_data;
g_assert (IDE_IS_COMPLETION_LIST_BOX_ROW (widget));
g_assert (state != NULL);
if (state->position == state->selected)
gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_SELECTED, FALSE);
else
gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_SELECTED);
if (state->context != NULL && state->position < state->n_items)
ide_completion_context_get_item_full (state->context, state->position, &provider, &proposal);
ide_completion_list_box_row_set_proposal (IDE_COMPLETION_LIST_BOX_ROW (widget), proposal);
if (provider && proposal)
{
g_autofree gchar *typed_text = NULL;
GtkTextIter begin, end;
if (ide_completion_context_get_bounds (state->context, &begin, &end))
typed_text = gtk_text_iter_get_slice (&begin, &end);
ide_completion_provider_display_proposal (provider,
IDE_COMPLETION_LIST_BOX_ROW (widget),
state->context,
typed_text,
proposal);
}
gtk_widget_set_visible (widget, proposal != NULL);
state->position++;
}
static gboolean
ide_completion_list_box_update_cb (GtkWidget *widget,
GdkFrameClock *frame_clock,
gpointer user_data)
{
IdeCompletionListBox *self = (IdeCompletionListBox *)widget;
UpdateState state = {0};
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
state.self = self;
state.context = self->context;
state.position = ide_completion_list_box_get_offset (self);
state.selected = self->selected;
if (self->context != NULL)
state.n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context));
state.position = MIN (state.position, MAX (state.n_items, self->n_rows) - self->n_rows);
state.selected = MIN (self->selected, state.n_items ? state.n_items - 1 : 0);
if (gtk_adjustment_get_upper (self->vadjustment) != state.n_items)
gtk_adjustment_set_upper (self->vadjustment, state.n_items);
gtk_container_foreach (GTK_CONTAINER (self->box),
ide_completion_list_box_update_row_cb,
&state);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROPOSAL]);
g_signal_emit (self, signals [REPOSITION], 0);
/* Do this last so that we block any follow-up queue_updates */
self->queued_update = 0;
return G_SOURCE_REMOVE;
}
static void
ide_completion_list_box_queue_update (IdeCompletionListBox *self)
{
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
if (self->queued_update == 0)
{
self->queued_update = gtk_widget_add_tick_callback (GTK_WIDGET (self),
ide_completion_list_box_update_cb,
NULL, NULL);
gtk_widget_queue_resize (GTK_WIDGET (self));
}
}
GtkWidget *
ide_completion_list_box_new (void)
{
return g_object_new (IDE_TYPE_COMPLETION_LIST_BOX, NULL);
}
guint
ide_completion_list_box_get_n_rows (IdeCompletionListBox *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), 0);
return self->n_rows;
}
void
ide_completion_list_box_set_n_rows (IdeCompletionListBox *self,
guint n_rows)
{
g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
g_return_if_fail (n_rows > 0);
g_return_if_fail (n_rows <= 32);
if (n_rows != self->n_rows)
{
gtk_container_foreach (GTK_CONTAINER (self->box),
(GtkCallback)gtk_widget_destroy,
NULL);
self->n_rows = n_rows;
if (self->vadjustment != NULL)
gtk_adjustment_set_page_size (self->vadjustment, n_rows);
for (guint i = 0; i < n_rows; i++)
{
GtkWidget *row = ide_completion_list_box_row_new ();
_ide_completion_list_box_row_attach (IDE_COMPLETION_LIST_BOX_ROW (row),
self->left_size_group,
self->center_size_group,
self->right_size_group);
_ide_completion_list_box_row_set_attrs (IDE_COMPLETION_LIST_BOX_ROW (row),
self->font_attrs);
gtk_container_add (GTK_CONTAINER (self), row);
}
ide_completion_list_box_queue_update (self);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_ROWS]);
}
}
/**
* ide_completion_list_box_get_proposal:
* @self: a #IdeCompletionListBox
*
* Gets the currently selected proposal, or %NULL if no proposal is selected
*
* Returns: (nullable) (transfer full): a #IdeCompletionProposal or %NULL
*
* Since: 3.32
*/
IdeCompletionProposal *
ide_completion_list_box_get_proposal (IdeCompletionListBox *self)
{
IdeCompletionProposal *ret = NULL;
g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), NULL);
if (self->context != NULL &&
self->selected < g_list_model_get_n_items (G_LIST_MODEL (self->context)))
ret = g_list_model_get_item (G_LIST_MODEL (self->context), self->selected);
g_return_val_if_fail (!ret || IDE_IS_COMPLETION_PROPOSAL (ret), NULL);
return ret;
}
/**
* ide_completion_list_box_get_selected:
* @self: an #IdeCompletionListBox
* @provider: (out) (transfer full) (optional): a location for the provider
* @proposal: (out) (transfer full) (optional): a location for the proposal
*
* Gets the selected item if there is any.
*
* If there is a selection, %TRUE is returned and @provider and @proposal
* are set to the selected provider/proposal.
*
* Returns: %TRUE if there is a selection
*
* Since: 3.32
*/
gboolean
ide_completion_list_box_get_selected (IdeCompletionListBox *self,
IdeCompletionProvider **provider,
IdeCompletionProposal **proposal)
{
g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), FALSE);
if (self->context != NULL)
{
guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context));
if (n_items > 0)
{
guint selected = MIN (self->selected, n_items - 1);
ide_completion_context_get_item_full (self->context, selected, provider, proposal);
return TRUE;
}
}
return FALSE;
}
/**
* ide_completion_list_box_get_context:
* @self: a #IdeCompletionListBox
*
* Gets the context that is being displayed in the list box.
*
* Returns: (transfer none) (nullable): an #IdeCompletionContext or %NULL
*
* Since: 3.32
*/
IdeCompletionContext *
ide_completion_list_box_get_context (IdeCompletionListBox *self)
{
g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), NULL);
return self->context;
}
static void
ide_completion_list_box_items_changed_cb (IdeCompletionListBox *self,
guint position,
guint removed,
guint added,
GListModel *model)
{
guint offset;
g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
g_assert (G_IS_LIST_MODEL (model));
offset = ide_completion_list_box_get_offset (self);
/* Skip widget resize if results are not visible */
if (position >= offset + self->n_rows)
return;
ide_completion_list_box_queue_update (self);
}
/**
* ide_completion_list_box_set_context:
* @self: a #IdeCompletionListBox
* @context: the #IdeCompletionContext
*
* Sets the context to be displayed.
*
* Since: 3.32
*/
void
ide_completion_list_box_set_context (IdeCompletionListBox *self,
IdeCompletionContext *context)
{
g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
if (self->context == context)
return;
if (self->context != NULL && self->items_changed_handler != 0)
{
g_signal_handler_disconnect (self->context, self->items_changed_handler);
self->items_changed_handler = 0;
}
g_set_object (&self->context, context);
if (self->context != NULL)
self->items_changed_handler =
g_signal_connect_object (self->context,
"items-changed",
G_CALLBACK (ide_completion_list_box_items_changed_cb),
self,
G_CONNECT_SWAPPED);
self->selected = 0;
gtk_adjustment_set_value (self->vadjustment, 0);
ide_completion_list_box_queue_update (self);
g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
}
static void
get_first_cb (GtkWidget *widget,
gpointer user_data)
{
GtkWidget **row = user_data;
if (*row == NULL)
*row = widget;
}
IdeCompletionListBoxRow *
_ide_completion_list_box_get_first (IdeCompletionListBox *self)
{
IdeCompletionListBoxRow *row = NULL;
g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), NULL);
gtk_container_foreach (GTK_CONTAINER (self->box), get_first_cb, &row);
return row;
}
void
ide_completion_list_box_move_cursor (IdeCompletionListBox *self,
GtkMovementStep step,
gint direction)
{
gint n_items;
gint offset;
g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
if (self->context == NULL || direction == 0)
return;
if (!(n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context))))
return;
/* n_items is signed so that we can do negative comparison */
if (n_items < 0)
return;
if (step == GTK_MOVEMENT_BUFFER_ENDS)
{
if (direction > 0)
{
ide_completion_list_box_set_offset (self, n_items);
self->selected = n_items - 1;
}
else
{
ide_completion_list_box_set_offset (self, 0);
self->selected = 0;
}
ide_completion_list_box_queue_update (self);
return;
}
if (direction < 0 && self->selected == 0)
return;
if (direction > 0 && self->selected == n_items - 1)
return;
if (step == GTK_MOVEMENT_PAGES)
direction *= self->n_rows;
if ((self->selected + direction) > n_items)
self->selected = n_items - 1;
else if ((self->selected + direction) < 0)
self->selected = 0;
else
self->selected += direction;
offset = ide_completion_list_box_get_offset (self);
if (self->selected < offset)
ide_completion_list_box_set_offset (self, self->selected);
else if (self->selected >= (offset + self->n_rows))
ide_completion_list_box_set_offset (self, self->selected - self->n_rows + 1);
ide_completion_list_box_queue_update (self);
}
gboolean
_ide_completion_list_box_key_activates (IdeCompletionListBox *self,
const GdkEventKey *key)
{
g_autoptr(IdeCompletionProvider) provider = NULL;
g_autoptr(IdeCompletionProposal) proposal = NULL;
g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), FALSE);
g_return_val_if_fail (key != NULL, FALSE);
if (ide_completion_list_box_get_selected (self, &provider, &proposal))
{
if (ide_completion_provider_key_activates (provider, proposal, key))
return TRUE;
}
return FALSE;
}
static void
update_font_desc (GtkWidget *widget,
gpointer user_data)
{
PangoAttrList *attrs = user_data;
if (IDE_IS_COMPLETION_LIST_BOX_ROW (widget))
_ide_completion_list_box_row_set_attrs (IDE_COMPLETION_LIST_BOX_ROW (widget), attrs);
}
void
_ide_completion_list_box_set_font_desc (IdeCompletionListBox *self,
const PangoFontDescription *font_desc)
{
g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
g_clear_pointer (&self->font_attrs, pango_attr_list_unref);
if (font_desc)
{
self->font_attrs = pango_attr_list_new ();
if (font_desc)
pango_attr_list_insert (self->font_attrs, pango_attr_font_desc_new (font_desc));
}
gtk_container_foreach (GTK_CONTAINER (self->box), update_font_desc, self->font_attrs);
}