gem-graph-client/libide/sourceview/ide-completion-view.c

444 lines
15 KiB
C

/* ide-completion-view.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-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);
}