/* ide-completion-view.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-view" #include "config.h" #include "ide-completion.h" #include "ide-completion-context.h" #include "ide-completion-list-box.h" #include "ide-completion-private.h" #include "ide-completion-proposal.h" #include "ide-completion-provider.h" #include "ide-completion-view.h" struct _IdeCompletionView { DzlBin parent_instance; IdeCompletionContext *context; IdeCompletionListBox *list_box; GtkLabel *details; }; enum { PROP_0, PROP_CONTEXT, PROP_PROPOSAL, N_PROPS }; enum { ACTIVATE, MOVE_CURSOR, REPOSITION, N_SIGNALS }; G_DEFINE_FINAL_TYPE (IdeCompletionView, ide_completion_view, DZL_TYPE_BIN) static GParamSpec *properties [N_PROPS]; static guint signals [N_SIGNALS]; static void ide_completion_view_real_activate (IdeCompletionView *self) { g_autoptr(IdeCompletionProvider) provider = NULL; g_autoptr(IdeCompletionProposal) proposal = NULL; IdeCompletion *completion; g_assert (IDE_IS_COMPLETION_VIEW (self)); if (self->context == NULL || !gtk_widget_get_visible (GTK_WIDGET (self)) || !(completion = ide_completion_context_get_completion (self->context)) || !ide_completion_list_box_get_selected (self->list_box, &provider, &proposal)) return; _ide_completion_activate (completion, self->context, provider, proposal); } static void ide_completion_view_real_move_cursor (IdeCompletionView *self, GtkMovementStep step, gint direction) { g_assert (IDE_IS_COMPLETION_VIEW (self)); if (!gtk_widget_get_visible (GTK_WIDGET (self))) return; ide_completion_list_box_move_cursor (self->list_box, step, direction); } static void on_notify_proposal_cb (IdeCompletionView *self, GParamSpec *pspec, IdeCompletionListBox *list_box) { g_autoptr(IdeCompletionProposal) proposal = NULL; g_autoptr(IdeCompletionProvider) provider = NULL; g_autofree gchar *comment = NULL; g_assert (IDE_IS_COMPLETION_VIEW (self)); g_assert (pspec != NULL); g_assert (IDE_IS_COMPLETION_LIST_BOX (list_box)); if (ide_completion_list_box_get_selected (list_box, &provider, &proposal)) comment = ide_completion_provider_get_comment (provider, proposal); gtk_label_set_label (self->details, comment); gtk_widget_set_visible (GTK_WIDGET (self->details), comment && *comment); } static void ide_completion_view_notify_proposal_cb (IdeCompletionListBox *list_box, GParamSpec *pspec, IdeCompletionView *view) { g_assert (IDE_IS_COMPLETION_LIST_BOX (list_box)); g_assert (IDE_IS_COMPLETION_VIEW (view)); g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_PROPOSAL]); } static void ide_completion_view_reposition_cb (IdeCompletionListBox *list_box, IdeCompletionView *view) { g_assert (IDE_IS_COMPLETION_VIEW (view)); g_assert (IDE_IS_COMPLETION_LIST_BOX (list_box)); g_signal_emit (view, signals [REPOSITION], 0); } static void ide_completion_view_finalize (GObject *object) { IdeCompletionView *self = (IdeCompletionView *)object; g_clear_object (&self->context); G_OBJECT_CLASS (ide_completion_view_parent_class)->finalize (object); } static void ide_completion_view_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeCompletionView *self = IDE_COMPLETION_VIEW (object); switch (prop_id) { case PROP_CONTEXT: g_value_set_object (value, ide_completion_view_get_context (self)); break; case PROP_PROPOSAL: g_value_take_object (value, ide_completion_list_box_get_proposal (self->list_box)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_completion_view_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { IdeCompletionView *self = IDE_COMPLETION_VIEW (object); switch (prop_id) { case PROP_CONTEXT: ide_completion_view_set_context (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_completion_view_class_init (IdeCompletionViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkBindingSet *binding_set = gtk_binding_set_by_class (klass); object_class->finalize = ide_completion_view_finalize; object_class->get_property = ide_completion_view_get_property; object_class->set_property = ide_completion_view_set_property; properties [PROP_CONTEXT] = g_param_spec_object ("context", "Context", "The context to display in the view", IDE_TYPE_COMPLETION_CONTEXT, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); properties [PROP_PROPOSAL] = g_param_spec_object ("proposal", "Proposal", "The selected proposal", IDE_TYPE_COMPLETION_PROPOSAL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, N_PROPS, properties); /** * IdeCompletionOverlay::move-cursor: * @self: an #IdeCompletionOverlay * @direction: the amount to move and in what direction * * Make @direction positive to move forward, negative to move backwards * * Since: 3.32 */ signals [MOVE_CURSOR] = g_signal_new_class_handler ("move-cursor", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (ide_completion_view_real_move_cursor), NULL, NULL, NULL, G_TYPE_NONE, 2, GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT); /** * IdeCompletionOverlay::activate: * @self: an #IdeCompletionOverlay * * Activates the selected item in the completion window. * * Since: 3.32 */ signals [ACTIVATE] = g_signal_new_class_handler ("activate", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (ide_completion_view_real_activate), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); g_signal_set_va_marshaller (signals [ACTIVATE], G_TYPE_FROM_CLASS (klass), g_cclosure_marshal_VOID__VOIDv); /** * IdeCompletionView::reposition: * * Signal used to request the the container reposition itself due * to changes in the underlying list. * * Since: 3.32 */ 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); widget_class->activate_signal = signals [ACTIVATE]; gtk_widget_class_set_css_name (widget_class, "completionview"); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-sourceview/ui/ide-completion-view.ui"); gtk_widget_class_bind_template_child (widget_class, IdeCompletionView, details); gtk_widget_class_bind_template_child (widget_class, IdeCompletionView, list_box); gtk_widget_class_bind_template_callback (widget_class, on_notify_proposal_cb); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Down, 0, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_DISPLAY_LINES, G_TYPE_INT, 1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Up, 0, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_DISPLAY_LINES, G_TYPE_INT, -1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Down, 0, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES, G_TYPE_INT, 1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Page_Down, 0, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES, G_TYPE_INT, 1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Up, 0, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES, G_TYPE_INT, -1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Page_Up, 0, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES, G_TYPE_INT, -1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Home, GDK_CONTROL_MASK, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_BUFFER_ENDS, G_TYPE_INT, -1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_End, GDK_CONTROL_MASK, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_BUFFER_ENDS, G_TYPE_INT, 1); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Up, GDK_CONTROL_MASK, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES, G_TYPE_INT, -5); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Down, GDK_CONTROL_MASK, "move-cursor", 2, GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES, G_TYPE_INT, 5); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Return, 0, "activate", 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Tab, 0, "activate", 0); g_type_ensure (IDE_TYPE_COMPLETION_LIST_BOX); } static void ide_completion_view_init (IdeCompletionView *self) { gtk_widget_init_template (GTK_WIDGET (self)); g_signal_connect (self->list_box, "notify::proposal", G_CALLBACK (ide_completion_view_notify_proposal_cb), self); g_signal_connect (self->list_box, "reposition", G_CALLBACK (ide_completion_view_reposition_cb), self); } /** * ide_completion_view_get_context: * @self: a #IdeCompletionView * * Gets the #IdeCompletionView:context property. * * Returns: (transfer none) (nullable): an #IdeCompletionContext or %NULL * * Since: 3.32 */ IdeCompletionContext * ide_completion_view_get_context (IdeCompletionView *self) { g_return_val_if_fail (IDE_IS_COMPLETION_VIEW (self), NULL); return self->context; } /** * ide_completion_view_set_context: * @self: a #IdeCompletionView * * Sets the #IdeCompletionContext to be visualized. * * Since: 3.32 */ void ide_completion_view_set_context (IdeCompletionView *self, IdeCompletionContext *context) { g_return_if_fail (IDE_IS_COMPLETION_VIEW (self)); g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context)); if (g_set_object (&self->context, context)) { ide_completion_list_box_set_context (self->list_box, context); gtk_widget_queue_resize (GTK_WIDGET (self)); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]); } } void _ide_completion_view_set_n_rows (IdeCompletionView *self, guint n_rows) { g_return_if_fail (IDE_IS_COMPLETION_VIEW (self)); g_return_if_fail (n_rows > 0); g_return_if_fail (n_rows <= 32); ide_completion_list_box_set_n_rows (self->list_box, n_rows); } gint _ide_completion_view_get_x_offset (IdeCompletionView *self) { IdeCompletionListBoxRow *first; g_return_val_if_fail (IDE_IS_COMPLETION_VIEW (self), 0); if ((first = _ide_completion_list_box_get_first (self->list_box))) return _ide_completion_list_box_row_get_x_offset (first, GTK_WIDGET (self)); return 0; } gboolean _ide_completion_view_handle_key_press (IdeCompletionView *self, const GdkEventKey *event) { GtkBindingSet *binding_set; GtkTextView *view; g_return_val_if_fail (IDE_IS_COMPLETION_VIEW (self), GDK_EVENT_PROPAGATE); g_return_val_if_fail (event != NULL, GDK_EVENT_PROPAGATE); /* * If we have a snippet active, we don't want to activate with tab since * that could advance the snippet (and should take precedence). */ if (self->context != NULL && event->keyval == GDK_KEY_Tab && (view = ide_completion_context_get_view (self->context)) && ide_source_view_has_snippet (IDE_SOURCE_VIEW (view))) return FALSE; /* The key-press might cause the proposal to activate as well as insert some * extra data. For example, a C completion provider might convert '.' to '->' * after inserting the completion. */ if (_ide_completion_list_box_key_activates (self->list_box, event)) { gtk_widget_activate (GTK_WIDGET (self)); return GDK_EVENT_STOP; } binding_set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (self)); return gtk_binding_set_activate (binding_set, event->keyval, event->state, G_OBJECT (self)); } void _ide_completion_view_move_cursor (IdeCompletionView *self, GtkMovementStep step, gint count) { g_return_if_fail (IDE_IS_COMPLETION_VIEW (self)); g_signal_emit (self, signals [MOVE_CURSOR], 0, step, count); } void _ide_completion_view_set_font_desc (IdeCompletionView *self, const PangoFontDescription *font_desc) { g_assert (IDE_IS_COMPLETION_VIEW (self)); _ide_completion_list_box_set_font_desc (self->list_box, font_desc); }