/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4; coding: utf-8 -*- * * moolineview.c * * Copyright (C) 2004-2005 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/moolineview.h" #include "mooutils/moomarshals.h" #include "mooutils/mooutils-gobject.h" #include "mooutils/moocompat.h" #include struct _MooLineViewPrivate { GHashTable *line_data; gboolean busy; gboolean scrolled; GtkTextMark *end_mark; }; typedef struct { guint ref_count; gpointer data; GDestroyNotify free_func; } LineData; static LineData *line_data_new (gpointer data, GDestroyNotify free_func); static LineData *line_data_ref (LineData *line_data); static void line_data_unref (LineData *line_data); static void moo_line_view_finalize (GObject *object); static void moo_line_view_realize (GtkWidget *widget); static gboolean moo_line_view_button_release (GtkWidget *widget, GdkEventButton *event); static void moo_line_view_move_cursor (GtkTextView *text_view, GtkMovementStep step, gint count, gboolean extend_selection); static void moo_line_view_populate_popup (GtkTextView *text_view, GtkMenu *menu); static void activate (MooLineView *view, int line); static void activate_current_line (MooLineView *view); static GtkTextBuffer *get_buffer (MooLineView *view); static GHashTable *get_hash_table (MooLineView *view); enum { ACTIVATE, ACTIVATE_CURRENT_LINE, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; enum { PROP_0, }; /* MOO_TYPE_LINE_VIEW */ G_DEFINE_TYPE (MooLineView, moo_line_view, MOO_TYPE_TEXT_VIEW) static void moo_line_view_class_init (MooLineViewClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkTextViewClass *textview_class = GTK_TEXT_VIEW_CLASS (klass); GtkBindingSet *binding_set; // gobject_class->set_property = moo_line_view_set_property; // gobject_class->get_property = moo_line_view_get_property; gobject_class->finalize = moo_line_view_finalize; widget_class->realize = moo_line_view_realize; widget_class->button_release_event = moo_line_view_button_release; textview_class->move_cursor = moo_line_view_move_cursor; textview_class->populate_popup = moo_line_view_populate_popup; signals[ACTIVATE] = g_signal_new ("activate", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (MooLineViewClass, activate), NULL, NULL, _moo_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT); signals[ACTIVATE_CURRENT_LINE] = moo_signal_new_cb ("activate-current-line", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK (activate_current_line), NULL, NULL, _moo_marshal_VOID__VOID, G_TYPE_NONE, 0); binding_set = gtk_binding_set_by_class (klass); gtk_binding_entry_add_signal (binding_set, GDK_Return, 0, "activate-current-line", 0); } static void moo_line_view_init (MooLineView *view) { view->priv = g_new0 (MooLineViewPrivate, 1); g_object_set (view, "editable", FALSE, "cursor-visible", FALSE, "current-line-color", "grey", "check-brackets", FALSE, NULL); } static void moo_line_view_finalize (GObject *object) { MooLineView *view = MOO_LINE_VIEW (object); if (view->priv->line_data) g_hash_table_destroy (view->priv->line_data); g_free (view->priv); G_OBJECT_CLASS (moo_line_view_parent_class)->finalize (object); } GtkWidget* moo_line_view_new (void) { return g_object_new (MOO_TYPE_LINE_VIEW, NULL); } void moo_line_view_clear (MooLineView *view) { GtkTextBuffer *buffer; GtkTextIter start, end; g_return_if_fail (MOO_IS_LINE_VIEW (view)); buffer = get_buffer (view); gtk_text_buffer_get_bounds (buffer, &start, &end); gtk_text_buffer_delete (buffer, &start, &end); if (view->priv->line_data) { g_hash_table_destroy (view->priv->line_data); view->priv->line_data = NULL; } } static GtkTextBuffer* get_buffer (MooLineView *view) { return gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); } static LineData* line_data_new (gpointer data, GDestroyNotify free_func) { LineData *line_data; g_return_val_if_fail (data != NULL, NULL); line_data = g_new (LineData, 1); line_data->ref_count = 1; line_data->data = data; line_data->free_func = free_func; return line_data; } static LineData* line_data_ref (LineData *line_data) { g_return_val_if_fail (line_data != NULL, NULL); line_data->ref_count++; return line_data; } static void line_data_unref (LineData *line_data) { if (line_data && !(--line_data->ref_count)) { if (line_data->data && line_data->free_func) line_data->free_func (line_data->data); g_free (line_data); } } static void free_g_value (GValue *value) { if (G_IS_VALUE (value)) { g_value_unset (value); g_free (value); } } static GHashTable* get_hash_table (MooLineView *view) { if (!view->priv->line_data) view->priv->line_data = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) free_g_value); return view->priv->line_data; } static gboolean moo_line_view_button_release (GtkWidget *widget, GdkEventButton *event) { GtkTextView *textview = GTK_TEXT_VIEW (widget); MooLineView *view = MOO_LINE_VIEW (widget); int buffer_x, buffer_y; GtkTextIter iter; gboolean result; result = GTK_WIDGET_CLASS(moo_line_view_parent_class)->button_release_event (widget, event); if (gtk_text_view_get_window_type (textview, event->window) == GTK_TEXT_WINDOW_TEXT && !moo_text_view_has_selection (MOO_TEXT_VIEW (widget))) { gtk_text_view_window_to_buffer_coords (textview, GTK_TEXT_WINDOW_TEXT, event->x, event->y, &buffer_x, &buffer_y); /* XXX */ gtk_text_view_get_line_at_y (textview, &iter, buffer_y, NULL); activate (view, gtk_text_iter_get_line (&iter)); } return result; } static void activate (MooLineView *view, int line) { g_signal_emit (view, signals[ACTIVATE], 0, line); } static int get_current_line (MooLineView *view) { GtkTextIter iter; GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)); gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer)); return gtk_text_iter_get_line (&iter); } static void activate_current_line (MooLineView *view) { activate (view, get_current_line (view)); } static int get_visible_height (GtkTextView *text_view) { GdkRectangle rect; GtkTextIter iter; int start, end; gtk_text_view_get_visible_rect (text_view, &rect); gtk_text_view_get_line_at_y (text_view, &iter, rect.y, NULL); start = gtk_text_iter_get_line (&iter); gtk_text_view_get_line_at_y (text_view, &iter, rect.y + rect.height - 1, NULL); end = gtk_text_iter_get_line (&iter); return end - start + 1; } static void moo_line_view_move_cursor (GtkTextView *text_view, GtkMovementStep step, gint count, gboolean extend_selection) { gboolean handle; MooLineView *view; GtkTextBuffer *buffer; int current_line, new_line = 0, height, total; GtkTextIter iter; switch (step) { case GTK_MOVEMENT_LOGICAL_POSITIONS: case GTK_MOVEMENT_VISUAL_POSITIONS: case GTK_MOVEMENT_WORDS: case GTK_MOVEMENT_PARAGRAPH_ENDS: case GTK_MOVEMENT_HORIZONTAL_PAGES: handle = FALSE; break; default: handle = TRUE; } if (extend_selection) handle = FALSE; if (!handle) return GTK_TEXT_VIEW_CLASS(moo_line_view_parent_class)->move_cursor (text_view, step, count, extend_selection); view = MOO_LINE_VIEW (text_view); buffer = gtk_text_view_get_buffer (text_view); gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer)); current_line = get_current_line (view); height = get_visible_height (text_view); total = gtk_text_buffer_get_line_count (buffer); switch (step) { case GTK_MOVEMENT_DISPLAY_LINES: case GTK_MOVEMENT_PARAGRAPHS: new_line = current_line + count; break; case GTK_MOVEMENT_PAGES: new_line = current_line + count * (height - 1); break; case GTK_MOVEMENT_DISPLAY_LINE_ENDS: case GTK_MOVEMENT_BUFFER_ENDS: if (count < 0) new_line = 0; else new_line = total - 1; break; case GTK_MOVEMENT_LOGICAL_POSITIONS: case GTK_MOVEMENT_VISUAL_POSITIONS: case GTK_MOVEMENT_WORDS: case GTK_MOVEMENT_PARAGRAPH_ENDS: case GTK_MOVEMENT_HORIZONTAL_PAGES: g_return_if_reached (); } new_line = CLAMP (new_line, 0, total - 1); gtk_text_buffer_get_iter_at_line (buffer, &iter, new_line); gtk_text_buffer_place_cursor (buffer, &iter); gtk_text_view_scroll_to_mark (text_view, gtk_text_buffer_get_insert (buffer), 0, FALSE, 0, 0); } static void moo_line_view_realize (GtkWidget *widget) { GdkWindow *window; GTK_WIDGET_CLASS(moo_line_view_parent_class)->realize (widget); window = gtk_text_view_get_window (GTK_TEXT_VIEW (widget), GTK_TEXT_WINDOW_TEXT); gdk_window_set_cursor (window, NULL); } /* XXX check line */ void moo_line_view_set_data (MooLineView *view, int line, gpointer data, GDestroyNotify free_func) { g_return_if_fail (MOO_IS_LINE_VIEW (view)); g_return_if_fail (line >= 0); if (data) { GValue val; LineData *line_data; val.g_type = 0; g_value_init (&val, MOO_TYPE_LINE_VIEW_DATA); line_data = line_data_new (data, free_func); g_value_set_boxed (&val, line_data); moo_line_view_set_line_data (view, line, &val); line_data_unref (line_data); g_value_unset (&val); } else { moo_line_view_set_line_data (view, line, NULL); } } gpointer moo_line_view_get_data (MooLineView *view, int line) { GValue val; gpointer result = NULL; g_return_val_if_fail (MOO_IS_LINE_VIEW (view), NULL); g_return_val_if_fail (line >= 0, NULL); val.g_type = 0; if (!moo_line_view_get_line_data (view, line, &val)) return NULL; if (G_VALUE_HOLDS (&val, MOO_TYPE_LINE_VIEW_DATA)) { LineData *line_data = g_value_get_boxed (&val); result = line_data->data; } else { g_warning ("%s: uknown data type", G_STRLOC); result = NULL; } g_value_unset (&val); return result; } GtkTextTag* moo_line_view_create_tag (MooLineView *view, const char *tag_name, const char *first_property_name, ...) { GtkTextTag *tag; GtkTextBuffer *buffer; va_list list; g_return_val_if_fail (MOO_IS_LINE_VIEW (view), NULL); buffer = get_buffer (view); tag = gtk_text_buffer_create_tag (buffer, tag_name, NULL); g_return_val_if_fail (tag != NULL, NULL); if (first_property_name) { va_start (list, first_property_name); g_object_set_valist (G_OBJECT (tag), first_property_name, list); va_end (list); } return tag; } GtkTextTag* moo_line_view_lookup_tag (MooLineView *view, const char *tag_name) { GtkTextTagTable *table; GtkTextBuffer *buffer; g_return_val_if_fail (MOO_IS_LINE_VIEW (view), NULL); g_return_val_if_fail (tag_name != NULL, NULL); buffer = get_buffer (view); table = gtk_text_buffer_get_tag_table (buffer); return gtk_text_tag_table_lookup (table, tag_name); } int moo_line_view_write_line (MooLineView *view, const char *text, gssize len, GtkTextTag *tag) { int line; g_return_val_if_fail (MOO_IS_LINE_VIEW (view), -1); g_return_val_if_fail (text != NULL, -1); line = moo_line_view_start_line (view); g_return_val_if_fail (line >= 0, -1); moo_line_view_write (view, text, len, tag); moo_line_view_end_line (view); return line; } static void check_if_scrolled (MooLineView *view) { GdkWindow *text_window; GdkRectangle rect; GtkTextIter iter; int line; if (!GTK_WIDGET_REALIZED (view)) { view->priv->scrolled = FALSE; return; } text_window = gtk_text_view_get_window (GTK_TEXT_VIEW (view), GTK_TEXT_WINDOW_TEXT); gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (view), &rect); gtk_text_view_get_line_at_y (GTK_TEXT_VIEW (view), &iter, rect.y + rect.height - 1, NULL); line = gtk_text_iter_get_line (&iter); if (line + 2 < gtk_text_buffer_get_line_count (get_buffer (view))) view->priv->scrolled = TRUE; else view->priv->scrolled = FALSE; } int moo_line_view_start_line (MooLineView *view) { GtkTextBuffer *buffer; GtkTextIter iter; g_return_val_if_fail (MOO_IS_LINE_VIEW (view), -1); g_return_val_if_fail (!view->priv->busy, -1); view->priv->busy = TRUE; buffer = get_buffer (view); gtk_text_buffer_get_end_iter (buffer, &iter); check_if_scrolled (view); if (!gtk_text_iter_starts_line (&iter)) gtk_text_buffer_insert (buffer, &iter, "\n", 1); return gtk_text_iter_get_line (&iter); } void moo_line_view_write (MooLineView *view, const char *text, gssize len, GtkTextTag *tag) { GtkTextBuffer *buffer; GtkTextIter iter; g_return_if_fail (MOO_IS_LINE_VIEW (view)); g_return_if_fail (text != NULL); g_return_if_fail (view->priv->busy); buffer = get_buffer (view); gtk_text_buffer_get_end_iter (buffer, &iter); if (g_utf8_validate (text, len, NULL)) { gtk_text_buffer_insert_with_tags (buffer, &iter, text, len, tag, NULL); } else { char *text_utf8 = g_locale_to_utf8 (text, len, NULL, NULL, NULL); if (text_utf8) gtk_text_buffer_insert_with_tags (buffer, &iter, text_utf8, -1, tag, NULL); else g_warning ("%s: could not convert '%s' to utf8", G_STRLOC, text); g_free (text_utf8); } } static GtkTextMark* get_end_mark (MooLineView *view) { GtkTextIter iter; GtkTextBuffer *buffer = get_buffer (view); gtk_text_buffer_get_end_iter (buffer, &iter); gtk_text_iter_set_line_offset (&iter, 0); if (!view->priv->end_mark) view->priv->end_mark = gtk_text_buffer_create_mark (buffer, NULL, &iter, FALSE); else gtk_text_buffer_move_mark (buffer, view->priv->end_mark, &iter); return view->priv->end_mark; } void moo_line_view_end_line (MooLineView *view) { g_return_if_fail (MOO_IS_LINE_VIEW (view)); g_return_if_fail (view->priv->busy); view->priv->busy = FALSE; if (!view->priv->scrolled) gtk_text_view_scroll_mark_onscreen (GTK_TEXT_VIEW (view), get_end_mark (view)); } static void copy_clipboard (GtkTextView *text_view) { g_signal_emit_by_name (text_view, "copy-clipboard"); } static void moo_line_view_populate_popup (GtkTextView *text_view, GtkMenu *menu) { GtkWidget *item; gboolean has_selection, has_text; gtk_container_foreach (GTK_CONTAINER (menu), (GtkCallback) gtk_widget_destroy, NULL); item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL); g_signal_connect_swapped (item, "activate", G_CALLBACK (copy_clipboard), text_view); has_selection = moo_text_view_has_selection (MOO_TEXT_VIEW (text_view)); gtk_widget_set_sensitive (item, has_selection); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL); g_signal_connect_swapped (item, "activate", G_CALLBACK (moo_text_view_select_all), text_view); has_text = moo_text_view_has_text (MOO_TEXT_VIEW (text_view)); gtk_widget_set_sensitive (item, has_text); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); } GType moo_line_view_data_get_type (void) { static GType type = 0; if (!type) type = g_boxed_type_register_static ("MooLineViewData", (GBoxedCopyFunc) line_data_ref, (GBoxedFreeFunc) line_data_unref); return type; } void moo_line_view_set_line_data (MooLineView *view, int line, const GValue *data) { GHashTable *hash; g_return_if_fail (MOO_IS_LINE_VIEW (view)); g_return_if_fail (line >= 0); g_return_if_fail (!data || G_IS_VALUE (data)); hash = get_hash_table (view); if (data) { GValue *val = g_new0 (GValue, 1); g_value_init (val, G_VALUE_TYPE (data)); g_value_copy (data, val); g_hash_table_insert (hash, GINT_TO_POINTER (line), val); } else { g_hash_table_remove (hash, GINT_TO_POINTER (line)); } } gboolean moo_line_view_get_line_data (MooLineView *view, int line, GValue *dest) { GValue *val; g_return_val_if_fail (MOO_IS_LINE_VIEW (view), FALSE); g_return_val_if_fail (line >= 0, FALSE); g_return_val_if_fail (!G_IS_VALUE (dest), FALSE); if (!view->priv->line_data) return FALSE; val = g_hash_table_lookup (view->priv->line_data, GINT_TO_POINTER (line)); if (!val) return FALSE; if (dest) { g_value_init (dest, G_VALUE_TYPE (val)); g_value_copy (val, dest); } return TRUE; }