/* * mootextcompletion.c * * Copyright (C) 2004-2006 by Yevgen Muntyan * * 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 2 of the License, or * (at your option) any later version. * * See COPYING file that comes with this distribution. */ #include "mooedit/mootextcompletion.h" #include "mooedit/mootextpopup.h" #include "mooutils/moomarshals.h" #include "mooutils/eggregex.h" #include #include #include struct _MooTextCompletionPrivate { GtkTreeModel *model; GtkTreeModelFilter *filter; int text_column; MooTextCompletionFilterFunc filter_func; gpointer data; GDestroyNotify notify; EggRegex *regex; guint *parens; guint n_parens; char *line; char *text; guint text_len; GtkTextView *doc; GtkTextBuffer *buffer; GtkTextMark *start; GtkTextMark *end; MooTextPopup *popup; guint working : 1; guint in_update : 1; }; static void moo_text_completion_dispose (GObject *object); static gboolean moo_text_completion_update_real (MooTextCompletion *cmpl); static gboolean moo_text_completion_complete_real (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter); static void moo_text_completion_populate (MooTextCompletion *cmpl, GtkTextIter *match_start, GtkTextIter *match_end, const char *match, guint paren); static void moo_text_completion_emit_complete (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter); static void moo_text_completion_connect_popup (MooTextCompletion *cmpl); static void moo_text_completion_disconnect_popup(MooTextCompletion *cmpl); static gboolean moo_text_completion_empty (MooTextCompletion *cmpl); static gboolean moo_text_completion_unique (MooTextCompletion *cmpl, GtkTreeIter *iter); static gboolean filter_model_visible_func (GtkTreeModel *model, GtkTreeIter *iter, gpointer data); static gboolean default_filter_func (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); G_DEFINE_TYPE (MooTextCompletion, moo_text_completion, G_TYPE_OBJECT) enum { POPULATE, UPDATE, COMPLETE, NUM_SIGNALS }; static guint signals[NUM_SIGNALS]; static void moo_text_completion_dispose (GObject *object) { MooTextCompletion *cmpl = MOO_TEXT_COMPLETION (object); if (cmpl->priv) { moo_text_completion_set_doc (cmpl, NULL); moo_text_completion_set_model (cmpl, NULL); if (cmpl->priv->notify) cmpl->priv->notify (cmpl->priv->data); if (cmpl->priv->regex) egg_regex_unref (cmpl->priv->regex); if (cmpl->priv->popup) { moo_text_completion_disconnect_popup (cmpl); g_object_unref (cmpl->priv->popup); } g_free (cmpl->priv->parens); g_free (cmpl->priv->text); g_free (cmpl->priv->line); g_free (cmpl->priv); cmpl->priv = NULL; } G_OBJECT_CLASS(moo_text_completion_parent_class)->dispose (object); } static void moo_text_completion_class_init (MooTextCompletionClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->dispose = moo_text_completion_dispose; klass->complete = moo_text_completion_complete_real; klass->update = moo_text_completion_update_real; signals[POPULATE] = g_signal_new ("populate", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (MooTextCompletionClass, populate), NULL, NULL, _moo_marshal_VOID__BOXED_BOXED_STRING_UINT, G_TYPE_NONE, 4, GTK_TYPE_TEXT_ITER | G_SIGNAL_TYPE_STATIC_SCOPE, GTK_TYPE_TEXT_ITER | G_SIGNAL_TYPE_STATIC_SCOPE, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE, G_TYPE_UINT); signals[UPDATE] = g_signal_new ("update", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (MooTextCompletionClass, update), g_signal_accumulator_true_handled, NULL, _moo_marshal_BOOLEAN__VOID, G_TYPE_BOOLEAN, 0); signals[COMPLETE] = g_signal_new ("complete", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (MooTextCompletionClass, complete), g_signal_accumulator_true_handled, NULL, _moo_marshal_BOOLEAN__OBJECT_BOXED, G_TYPE_BOOLEAN, 2, GTK_TYPE_TREE_MODEL, GTK_TYPE_TREE_ITER | G_SIGNAL_TYPE_STATIC_SCOPE); } static void moo_text_completion_init (MooTextCompletion *cmpl) { cmpl->priv = g_new0 (MooTextCompletionPrivate, 1); cmpl->priv->text_column = -1; cmpl->priv->filter_func = default_filter_func; cmpl->priv->popup = moo_text_popup_new (NULL); moo_text_completion_connect_popup (cmpl); cmpl->priv->regex = egg_regex_new ("\\b\\w+$", 0, 0, NULL); cmpl->priv->parens = g_new0 (guint, 1); cmpl->priv->n_parens = 1; } static gboolean filter_model_visible_func (GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { MooTextCompletion *cmpl = data; return cmpl->priv->filter_func (cmpl, model, iter, cmpl->priv->data); } static gboolean default_filter_func (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter, G_GNUC_UNUSED gpointer dummy) { gboolean visible = TRUE; char *text = NULL; if (!cmpl->priv->text) return TRUE; g_return_val_if_fail (cmpl->priv->text_column >= 0, TRUE); gtk_tree_model_get (model, iter, cmpl->priv->text_column, &text, -1); g_return_val_if_fail (text != NULL, TRUE); if (cmpl->priv->text_len) visible = !strncmp (text, cmpl->priv->text, cmpl->priv->text_len); g_free (text); return visible; } MooTextCompletion * moo_text_completion_new (void) { return g_object_new (MOO_TYPE_TEXT_COMPLETION, NULL); } void moo_text_completion_try_complete (MooTextCompletion *cmpl) { GtkTextIter start, end; GtkTreeIter iter; gboolean found = FALSE; guint paren = 0; g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); g_return_if_fail (cmpl->priv->buffer != NULL); g_return_if_fail (cmpl->priv->model != NULL); g_return_if_fail (!cmpl->priv->working); cmpl->priv->working = TRUE; gtk_text_buffer_get_iter_at_mark (cmpl->priv->buffer, &end, gtk_text_buffer_get_insert (cmpl->priv->buffer)); start = end; if (!gtk_text_iter_starts_line (&start)) gtk_text_iter_set_line_offset (&start, 0); g_free (cmpl->priv->line); cmpl->priv->line = gtk_text_buffer_get_slice (cmpl->priv->buffer, &start, &end, TRUE); egg_regex_clear (cmpl->priv->regex); if (egg_regex_match (cmpl->priv->regex, cmpl->priv->line, -1, 0) >= 1) { guint i; for (i = 0; i < cmpl->priv->n_parens; ++i) { int start_pos = -1, end_pos = -1; egg_regex_fetch_pos (cmpl->priv->regex, cmpl->priv->line, cmpl->priv->parens[i], &start_pos, &end_pos); if (start_pos >= 0 && end_pos >= 0) { found = TRUE; paren = i; gtk_text_iter_set_line_index (&start, start_pos); gtk_text_iter_set_line_index (&end, end_pos); break; } } } g_free (cmpl->priv->text); if (found) { cmpl->priv->text = gtk_text_buffer_get_slice (cmpl->priv->buffer, &start, &end, TRUE); cmpl->priv->text_len = strlen (cmpl->priv->text); moo_text_completion_set_region (cmpl, &start, &end); moo_text_completion_populate (cmpl, &start, &end, cmpl->priv->text, paren); } else { cmpl->priv->text = NULL; cmpl->priv->text_len = 0; moo_text_completion_set_region (cmpl, &end, &end); moo_text_completion_populate (cmpl, &end, &end, NULL, 0); } if (moo_text_completion_empty (cmpl)) { cmpl->priv->working = FALSE; return; } if (moo_text_completion_unique (cmpl, &iter)) { moo_text_completion_emit_complete (cmpl, GTK_TREE_MODEL (cmpl->priv->filter), &iter); cmpl->priv->working = FALSE; return; } moo_text_popup_show (cmpl->priv->popup, &start); } static gboolean moo_text_completion_update_real (MooTextCompletion *cmpl) { GtkTextIter start, end; g_return_val_if_fail (cmpl->priv->working, TRUE); moo_text_completion_get_region (cmpl, &start, &end); g_free (cmpl->priv->text); cmpl->priv->text = gtk_text_buffer_get_slice (cmpl->priv->buffer, &start, &end, TRUE); cmpl->priv->text_len = strlen (cmpl->priv->text); gtk_tree_model_filter_refilter (cmpl->priv->filter); return TRUE; } static gboolean moo_text_completion_complete_real (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter) { char *text = NULL; GtkTextIter start, end; g_return_val_if_fail (cmpl->priv->text_column >= 0, FALSE); gtk_tree_model_get (model, iter, cmpl->priv->text_column, &text, -1); g_return_val_if_fail (text != 0, FALSE); moo_text_completion_get_region (cmpl, &start, &end); gtk_text_buffer_begin_user_action (cmpl->priv->buffer); gtk_text_buffer_delete (cmpl->priv->buffer, &start, &end); gtk_text_buffer_insert (cmpl->priv->buffer, &start, text, -1); gtk_text_buffer_end_user_action (cmpl->priv->buffer); g_free (text); return TRUE; } static void moo_text_completion_populate (MooTextCompletion *cmpl, GtkTextIter *match_start, GtkTextIter *match_end, const char *match, guint paren) { g_signal_emit (cmpl, signals[POPULATE], 0, match_start, match_end, match, paren); } static void moo_text_completion_emit_complete (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter) { gboolean retval; g_signal_emit (cmpl, signals[COMPLETE], 0, model, iter, &retval); } static gboolean moo_text_completion_empty (MooTextCompletion *cmpl) { return !cmpl->priv->filter || !gtk_tree_model_iter_n_children (GTK_TREE_MODEL (cmpl->priv->filter), NULL); } static gboolean moo_text_completion_unique (MooTextCompletion *cmpl, GtkTreeIter *iter) { return cmpl->priv->filter && gtk_tree_model_iter_n_children (GTK_TREE_MODEL (cmpl->priv->filter), NULL) == 1 && gtk_tree_model_get_iter_first (GTK_TREE_MODEL (cmpl->priv->filter), iter); } static void moo_text_completion_update (MooTextCompletion *cmpl) { gboolean retval; if (!cmpl->priv->working || cmpl->priv->in_update) return; cmpl->priv->in_update = TRUE; g_signal_emit (cmpl, signals[UPDATE], 0, &retval); cmpl->priv->in_update = FALSE; if (moo_text_completion_empty (cmpl)) moo_text_completion_hide (cmpl); else moo_text_popup_update (cmpl->priv->popup); } static void on_popup_activate (MooTextCompletion *cmpl, GtkTreeModel *model, GtkTreeIter *iter) { moo_text_completion_emit_complete (cmpl, model, iter); } static void moo_text_completion_connect_popup (MooTextCompletion *cmpl) { g_signal_connect_swapped (cmpl->priv->popup, "activate", G_CALLBACK (on_popup_activate), cmpl); g_signal_connect_swapped (cmpl->priv->popup, "text-changed", G_CALLBACK (moo_text_completion_update), cmpl); g_signal_connect_swapped (cmpl->priv->popup, "hide", G_CALLBACK (moo_text_completion_hide), cmpl); } static void moo_text_completion_disconnect_popup (MooTextCompletion *cmpl) { g_signal_handlers_disconnect_by_func (cmpl->priv->popup, (gpointer) on_popup_activate, cmpl); g_signal_handlers_disconnect_by_func (cmpl->priv->popup, (gpointer) moo_text_completion_update, cmpl); g_signal_handlers_disconnect_by_func (cmpl->priv->popup, (gpointer) moo_text_completion_hide, cmpl); } void moo_text_completion_complete (MooTextCompletion *cmpl) { GtkTreeIter iter; g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); g_return_if_fail (cmpl->priv->working); if (moo_text_completion_unique (cmpl, &iter)) moo_text_completion_emit_complete (cmpl, GTK_TREE_MODEL (cmpl->priv->filter), &iter); else moo_text_popup_activate (cmpl->priv->popup); moo_text_completion_hide (cmpl); } void moo_text_completion_hide (MooTextCompletion *cmpl) { g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); if (cmpl->priv->working) { cmpl->priv->working = FALSE; moo_text_popup_hide (cmpl->priv->popup); } } void moo_text_completion_set_doc (MooTextCompletion *cmpl, GtkTextView *doc) { g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); g_return_if_fail (!doc || GTK_IS_TEXT_VIEW (doc)); if (cmpl->priv->doc == doc) return; moo_text_completion_hide (cmpl); g_free (cmpl->priv->line); g_free (cmpl->priv->text); cmpl->priv->line = NULL; cmpl->priv->text = NULL; cmpl->priv->text_len = 0; if (cmpl->priv->doc) { if (cmpl->priv->start) gtk_text_buffer_delete_mark (cmpl->priv->buffer, cmpl->priv->start); if (cmpl->priv->end) gtk_text_buffer_delete_mark (cmpl->priv->buffer, cmpl->priv->end); g_object_unref (cmpl->priv->doc); cmpl->priv->start = cmpl->priv->end = NULL; cmpl->priv->buffer = NULL; cmpl->priv->doc = NULL; } if (doc) { cmpl->priv->doc = g_object_ref (doc); cmpl->priv->buffer = gtk_text_view_get_buffer (doc); } moo_text_popup_set_doc (cmpl->priv->popup, doc); } GtkTextView * moo_text_completion_get_doc (MooTextCompletion *cmpl) { g_return_val_if_fail (MOO_IS_TEXT_COMPLETION (cmpl), NULL); return cmpl->priv->doc; } void moo_text_completion_set_model (MooTextCompletion *cmpl, GtkTreeModel *model) { g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); g_return_if_fail (!model || GTK_IS_TREE_MODEL (model)); if (cmpl->priv->model == model) return; moo_text_completion_hide (cmpl); if (cmpl->priv->model) { g_object_unref (cmpl->priv->filter); g_object_unref (cmpl->priv->model); cmpl->priv->filter = NULL; cmpl->priv->model = NULL; } if (model) { cmpl->priv->model = g_object_ref (model); cmpl->priv->filter = GTK_TREE_MODEL_FILTER ( gtk_tree_model_filter_new (cmpl->priv->model, NULL)); gtk_tree_model_filter_set_visible_func (cmpl->priv->filter, filter_model_visible_func, cmpl, NULL); } moo_text_popup_set_model (cmpl->priv->popup, GTK_TREE_MODEL (cmpl->priv->filter)); } GtkTreeModel * moo_text_completion_get_model (MooTextCompletion *cmpl) { g_return_val_if_fail (MOO_IS_TEXT_COMPLETION (cmpl), NULL); return cmpl->priv->model; } void moo_text_completion_set_filter_func (MooTextCompletion *cmpl, MooTextCompletionFilterFunc func, gpointer data, GDestroyNotify notify) { g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); if (!func) return moo_text_completion_set_filter_func (cmpl, default_filter_func, NULL, NULL); if (cmpl->priv->notify) cmpl->priv->notify (cmpl->priv->data); cmpl->priv->filter_func = func; cmpl->priv->notify = notify; cmpl->priv->data = data; moo_text_completion_update (cmpl); } void moo_text_completion_set_regex (MooTextCompletion *cmpl, const char *pattern, const guint *parens, guint n_parens) { EggRegex *regex; GError *error = NULL; char *real_pattern; g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); g_return_if_fail (pattern && pattern[0]); g_return_if_fail (!parens || n_parens); moo_text_completion_hide (cmpl); real_pattern = g_strdup_printf ("%s$", pattern); regex = egg_regex_new (real_pattern, 0, 0, &error); if (error) { g_warning ("%s: %s", G_STRLOC, error->message); goto err; } egg_regex_optimize (regex, &error); if (error) { g_warning ("%s: %s", G_STRLOC, error->message); g_error_free (error); } egg_regex_unref (cmpl->priv->regex); g_free (cmpl->priv->parens); if (!parens) { cmpl->priv->parens = g_new0 (guint, 1); cmpl->priv->n_parens = 1; } else { cmpl->priv->parens = g_memdup (parens, n_parens * sizeof (guint)); cmpl->priv->n_parens = n_parens; } return; err: if (error) g_error_free (error); g_free (real_pattern); egg_regex_unref (regex); } void moo_text_completion_set_text_column (MooTextCompletion *cmpl, int column) { g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); cmpl->priv->text_column = column; } MooTextPopup * moo_text_completion_get_popup (MooTextCompletion *cmpl) { g_return_val_if_fail (MOO_IS_TEXT_COMPLETION (cmpl), NULL); return cmpl->priv->popup; } gboolean moo_text_completion_get_region (MooTextCompletion *cmpl, GtkTextIter *start, GtkTextIter *end) { g_return_val_if_fail (MOO_IS_TEXT_COMPLETION (cmpl), FALSE); if (cmpl->priv->start) { if (start) gtk_text_buffer_get_iter_at_mark (cmpl->priv->buffer, start, cmpl->priv->start); if (end) gtk_text_buffer_get_iter_at_mark (cmpl->priv->buffer, end, cmpl->priv->end); return TRUE; } else { return FALSE; } } void moo_text_completion_set_region (MooTextCompletion *cmpl, const GtkTextIter *start, const GtkTextIter *end) { g_return_if_fail (MOO_IS_TEXT_COMPLETION (cmpl)); g_return_if_fail (start && end); g_return_if_fail (cmpl->priv->buffer != NULL); if (!cmpl->priv->start) { cmpl->priv->start = gtk_text_buffer_create_mark (cmpl->priv->buffer, NULL, start, TRUE); cmpl->priv->end = gtk_text_buffer_create_mark (cmpl->priv->buffer, NULL, end, FALSE); } else { gtk_text_buffer_move_mark (cmpl->priv->buffer, cmpl->priv->start, start); gtk_text_buffer_move_mark (cmpl->priv->buffer, cmpl->priv->end, end); } moo_text_popup_set_position (cmpl->priv->popup, start); moo_text_completion_update (cmpl); } MooTextCompletion * moo_text_completion_new_text (GtkTextView *doc, GtkTreeModel *model, int text_column) { MooTextCompletion *cmpl; GtkCellRenderer *cell; g_return_val_if_fail (GTK_IS_TREE_MODEL (model), NULL); g_return_val_if_fail (gtk_tree_model_get_column_type (model, text_column) == G_TYPE_STRING, NULL); cmpl = moo_text_completion_new (); moo_text_completion_set_model (cmpl, model); moo_text_completion_set_text_column (cmpl, text_column); cell = gtk_cell_renderer_text_new (); gtk_tree_view_column_pack_start (cmpl->priv->popup->column, cell, TRUE); gtk_tree_view_column_set_attributes (cmpl->priv->popup->column, cell, "text", text_column, NULL); if (doc) moo_text_completion_set_doc (cmpl, doc); return cmpl; }