/* ide-completion-list-box.c * * Copyright 2018-2019 Christian Hergert * * 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 . * * 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); }