444 lines
15 KiB
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);
|
|
}
|