/* * mooedit/mooeditcursor.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. */ #define MOOEDIT_COMPILATION #include "mooedit/mooedit-private.h" #include "mooedit/mootextiter.h" #include "mooutils/moocompat.h" inline static GtkWidgetClass *parent_class (void) { static gpointer sourceview_class = NULL; if (!sourceview_class) sourceview_class = GTK_WIDGET_CLASS (gtk_type_class (GTK_TYPE_SOURCE_VIEW)); return GTK_WIDGET_CLASS (sourceview_class); } inline static gboolean is_word_char (const GtkTextIter *iter) { gunichar c = gtk_text_iter_get_char (iter); g_return_val_if_fail (c != 0, FALSE); return g_unichar_isalnum (c) || c == '_'; } /* Glib docs say: "(Note: don't use this to do word breaking; you have * to use Pango or equivalent to get word breaking right, the algorithm * is fairly complex.)" */ inline static gboolean is_space (const GtkTextIter *iter) { gunichar c = gtk_text_iter_get_char (iter); g_return_val_if_fail (c != 0, FALSE); return g_unichar_isspace (c); } inline static gboolean is_word_start (const GtkTextIter *iter) { GtkTextIter i; if (!is_word_char (iter)) return FALSE; i = *iter; if (!gtk_text_iter_backward_char (&i)) return TRUE; return !is_word_char (&i); } static gboolean text_iter_forward_word_start (GtkTextIter *iter) { gboolean moved = FALSE; if (gtk_text_iter_is_end (iter)) return FALSE; /* if iter points to word char, then go to the first non-space char after the word * otherwise, go to the next word char * stop at end of line */ if (is_word_char (iter)) { while (!gtk_text_iter_is_end (iter) && is_word_char (iter)) { gtk_text_iter_forward_char (iter); moved = TRUE; } if (gtk_text_iter_is_end (iter)) return FALSE; while (!gtk_text_iter_is_end (iter) && is_space (iter) && !gtk_text_iter_ends_line (iter)) { gtk_text_iter_forward_char (iter); moved = TRUE; } } else { if (gtk_text_iter_ends_line (iter)) { gtk_text_iter_forward_char (iter); moved = TRUE; } else { while (!gtk_text_iter_is_end (iter) && !is_word_char (iter) && !gtk_text_iter_ends_line (iter)) { gtk_text_iter_forward_char (iter); moved = TRUE; } } } return moved && !gtk_text_iter_is_end (iter); } inline static gboolean text_iter_forward_word_start_n (GtkTextIter *iter, guint count) { if (!count) return FALSE; while (count) { if (!text_iter_forward_word_start (iter)) { gtk_text_iter_forward_to_end (iter); return FALSE; } else --count; } return TRUE; } inline static gboolean text_iter_backward_word_start (GtkTextIter *iter) { gboolean moved = FALSE; if (gtk_text_iter_starts_line (iter)) { moved = gtk_text_iter_backward_char (iter); /* it may point now to \n in \r\n combination */ if (moved && !gtk_text_iter_ends_line (iter)) gtk_text_iter_backward_char (iter); } else { while (gtk_text_iter_backward_char (iter) && !is_word_start (iter) && !gtk_text_iter_starts_line (iter)) moved = TRUE; } return moved; } inline static gboolean text_iter_backward_word_start_n (GtkTextIter *iter, guint count) { gboolean moved = FALSE; while (count && text_iter_backward_word_start (iter)) { moved = TRUE; --count; } return moved; } /* TODO: may I do this? */ inline static void text_view_reset_im_context (GtkTextView *text_view) { if (text_view->need_im_reset) { text_view->need_im_reset = FALSE; gtk_im_context_reset (text_view->im_context); } } void _moo_edit_move_cursor (GtkTextView *text_view, GtkMovementStep step, gint count, gboolean extend_selection) { GtkTextBuffer *buffer; GtkTextIter insert; GtkTextIter newplace; g_return_if_fail (text_view != NULL); if (!text_view->cursor_visible || step != GTK_MOVEMENT_WORDS) return GTK_TEXT_VIEW_CLASS (parent_class())->move_cursor ( text_view, step, count, extend_selection); buffer = gtk_text_view_get_buffer (text_view); text_view_reset_im_context (text_view); gtk_text_buffer_get_iter_at_mark (buffer, &insert, gtk_text_buffer_get_mark (buffer, "insert")); newplace = insert; if (count < 0) text_iter_backward_word_start_n (&newplace, -count); else if (count > 0) text_iter_forward_word_start_n (&newplace, count); /* call move_cursor() even if the cursor hasn't moved, since it cancels the selection */ /* move_cursor (buf, &newplace, extend_selection); */ if (extend_selection) gtk_text_buffer_move_mark (buffer, gtk_text_buffer_get_insert (buffer), &newplace); else gtk_text_buffer_place_cursor (buffer, &newplace); if (!gtk_text_iter_equal (&insert, &newplace)) { gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_insert (buffer)); } } void _moo_edit_delete_from_cursor (GtkTextView *text_view, GtkDeleteType type, gint count) { GtkTextBuffer *buf; GtkTextMark *insert_mark; GtkTextIter insert, start, end; if (type != GTK_DELETE_WORD_ENDS) return GTK_TEXT_VIEW_CLASS (parent_class())->delete_from_cursor (text_view, type, count); text_view_reset_im_context (text_view); buf = gtk_text_view_get_buffer (text_view); insert_mark = gtk_text_buffer_get_insert (buf); gtk_text_buffer_get_iter_at_mark (buf, &insert, insert_mark); start = insert; end = insert; if (count > 0) text_iter_forward_word_start_n (&end, count); else if (count < 0) text_iter_backward_word_start_n (&start, -count); if (!gtk_text_iter_equal (&start, &end)) { gtk_text_buffer_begin_user_action (buf); gtk_text_buffer_delete_interactive (buf, &start, &end, text_view->editable); gtk_text_buffer_end_user_action (buf); gtk_text_view_scroll_mark_onscreen (text_view, insert_mark); } } /****************************************************************************/ /* Mouse */ inline static gboolean extend_selection (MooEdit *edit, MooEditSelectionType type, GtkTextIter *insert, GtkTextIter *selection_bound) { return MOO_EDIT_GET_CLASS(edit)->extend_selection (edit, type, insert, selection_bound); } inline static void clear_drag_stuff (MooEdit *edit) { edit->priv->drag_moved = FALSE; edit->priv->drag_type = MOO_EDIT_DRAG_NONE; edit->priv->drag_start_x = -1; edit->priv->drag_start_y = -1; edit->priv->drag_button = GDK_BUTTON_RELEASE; } #define get_buf(edit) \ GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (edit))) /* from gtktextview.c */ inline static void text_view_unobscure_mouse_cursor (GtkTextView *text_view) { if (text_view->mouse_cursor_obscured) { GdkCursor *cursor = gdk_cursor_new_for_display (gtk_widget_get_display (GTK_WIDGET (text_view)), GDK_XTERM); gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), cursor); gdk_cursor_unref (cursor); text_view->mouse_cursor_obscured = FALSE; } } static void start_drag_scroll (MooEdit *edit); static void stop_drag_scroll (MooEdit *edit); static gboolean drag_scroll_timeout_func (MooEdit *edit); /* from gtktextview.c */ static void text_view_start_selection_dnd (GtkTextView *text_view, const GtkTextIter *iter, GdkEventMotion *event); static const int SCROLL_TIMEOUT = 100; int _moo_edit_button_press_event (GtkWidget *widget, GdkEventButton *event) { GtkTextView *text_view; MooEdit *edit; GtkTextBuffer *buffer; int x, y; GtkTextIter iter; text_view = GTK_TEXT_VIEW (widget); text_view_unobscure_mouse_cursor (text_view); if (gtk_text_view_get_window_type (text_view, event->window) != GTK_TEXT_WINDOW_TEXT) { return FALSE; } edit = MOO_EDIT (widget); buffer = gtk_text_view_get_buffer (text_view); gtk_widget_grab_focus (widget); text_view_reset_im_context (text_view); gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_TEXT, (int)event->x, (int)event->y, &x, &y); gtk_text_view_get_iter_at_location (text_view, &iter, x, y); if (event->type == GDK_BUTTON_PRESS) { if (event->button == 1) { GtkTextIter sel_start, sel_end; edit->priv->drag_button = GDK_BUTTON_PRESS; edit->priv->drag_start_x = x; edit->priv->drag_start_y = y; /* if clicked in selected, start drag */ if (gtk_text_buffer_get_selection_bounds (buffer, &sel_start, &sel_end)) { gtk_text_iter_order (&sel_start, &sel_end); if (gtk_text_iter_in_range (&iter, &sel_start, &sel_end)) { /* clicked inside of selection, * set up drag and return */ edit->priv->drag_type = MOO_EDIT_DRAG_DRAG; return TRUE; } } /* otherwise, clear selection, and position cursor at clicked point */ if (event->state & GDK_SHIFT_MASK) gtk_text_buffer_move_mark (buffer, gtk_text_buffer_get_insert (buffer), &iter); else gtk_text_buffer_place_cursor (buffer, &iter); edit->priv->drag_type = MOO_EDIT_DRAG_SELECT; } else if (event->button == 2 || event->button == 3) { /* let GtkSourceView worry about this */ return parent_class()->button_press_event (widget, event); } else { g_warning ("got button %d in button_press callback", event->button); } } else if (event->type == GDK_2BUTTON_PRESS && event->button == 1) { GtkTextIter bound; if (gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL)) { /* it may happen sometimes, if you click fast enough */ gtk_text_buffer_place_cursor (buffer, &iter); } edit->priv->drag_button = GDK_2BUTTON_PRESS; edit->priv->drag_start_x = x; edit->priv->drag_start_y = y; edit->priv->drag_type = MOO_EDIT_DRAG_SELECT; bound = iter; if (extend_selection (edit, MOO_EDIT_SELECT_WORDS, &iter, &bound)) gtk_text_buffer_select_range (buffer, &iter, &bound); } else if (event->type == GDK_3BUTTON_PRESS && event->button == 1) { GtkTextIter bound = iter; edit->priv->drag_button = GDK_3BUTTON_PRESS; edit->priv->drag_start_x = x; edit->priv->drag_start_y = y; edit->priv->drag_type = MOO_EDIT_DRAG_SELECT; if (extend_selection (edit, MOO_EDIT_SELECT_LINES, &iter, &bound)) gtk_text_buffer_select_range (buffer, &iter, &bound); } return TRUE; } int _moo_edit_button_release_event (GtkWidget *widget, G_GNUC_UNUSED GdkEventButton *event) { GtkTextView *text_view = GTK_TEXT_VIEW (widget); MooEdit *edit = MOO_EDIT (widget); GtkTextIter iter; switch (edit->priv->drag_type) { case MOO_EDIT_DRAG_NONE: /* it may happen after right-click, or clicking outside * of widget or something like that * everything has been taken care of, so do nothing */ break; case MOO_EDIT_DRAG_SELECT: /* everything should be done already in button_press and * motion_notify handlers */ stop_drag_scroll (edit); break; case MOO_EDIT_DRAG_DRAG: /* if we were really dragging, drop it * otherwise, it was just a single click in selected text */ g_assert (!edit->priv->drag_moved); /* parent should handle drag */ gtk_text_view_get_iter_at_location (text_view, &iter, edit->priv->drag_start_x, edit->priv->drag_start_y); gtk_text_buffer_place_cursor (gtk_text_view_get_buffer (text_view), &iter); break; default: g_assert_not_reached (); } clear_drag_stuff (edit); return TRUE; } #define outside(x,y,rect) \ ((x) < (rect).x || \ (y) < (rect).y || \ (x) >= (rect).x + (rect).width || \ (y) >= (rect).y + (rect).height) int _moo_edit_motion_event (GtkWidget *widget, GdkEventMotion *event) { GtkTextView *text_view = GTK_TEXT_VIEW (widget); MooEdit *edit = MOO_EDIT (widget); int x, y, event_x, event_y; GtkTextIter iter; text_view_unobscure_mouse_cursor (text_view); if (!edit->priv->drag_type) return FALSE; if (event->is_hint) gdk_window_get_pointer (event->window, &event_x, &event_y, NULL); else { event_x = (int)event->x; event_y = (int)event->y; } gtk_text_view_window_to_buffer_coords (text_view, gtk_text_view_get_window_type (text_view, event->window), event_x, event_y, &x, &y); gtk_text_view_get_iter_at_location (text_view, &iter, x, y); if (edit->priv->drag_type == MOO_EDIT_DRAG_SELECT) { GdkRectangle rect; GtkTextIter start; MooEditSelectionType t; GtkTextBuffer *buffer; edit->priv->drag_moved = TRUE; gtk_text_view_get_visible_rect (text_view, &rect); if (outside (x, y, rect)) { start_drag_scroll (edit); return TRUE; } else stop_drag_scroll (edit); buffer = gtk_text_view_get_buffer (text_view); gtk_text_view_get_iter_at_location (text_view, &start, edit->priv->drag_start_x, edit->priv->drag_start_y); if (edit->priv->drag_button == GDK_BUTTON_PRESS) t = MOO_EDIT_SELECT_CHARS; else if (edit->priv->drag_button == GDK_2BUTTON_PRESS) t = MOO_EDIT_SELECT_WORDS; else t = MOO_EDIT_SELECT_LINES; if (extend_selection (edit, t, &start, &iter)) gtk_text_buffer_select_range (buffer, &iter, &start); else gtk_text_buffer_place_cursor (buffer, &iter); } else { /* this piece is from gtktextview.c */ int x, y; gdk_window_get_pointer (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), &x, &y, NULL); if (gtk_drag_check_threshold (widget, edit->priv->drag_start_x, edit->priv->drag_start_y, x, y)) { GtkTextIter iter; int buffer_x, buffer_y; gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_TEXT, edit->priv->drag_start_x, edit->priv->drag_start_y, &buffer_x, &buffer_y); gtk_text_view_get_iter_at_location (text_view, &iter, buffer_x, buffer_y); edit->priv->drag_type = MOO_EDIT_DRAG_NONE; text_view_start_selection_dnd (text_view, &iter, event); } } return TRUE; } static const GtkTargetEntry gtk_text_view_target_table[] = { { (char*)"GTK_TEXT_BUFFER_CONTENTS", GTK_TARGET_SAME_APP, 0 } }; static void text_view_start_selection_dnd (GtkTextView *text_view, G_GNUC_UNUSED const GtkTextIter *iter, GdkEventMotion *event) { GdkDragContext *context; GtkTargetList *target_list; text_view->drag_start_x = -1; text_view->drag_start_y = -1; text_view->pending_place_cursor_button = 0; target_list = gtk_target_list_new (gtk_text_view_target_table, G_N_ELEMENTS (gtk_text_view_target_table)); gtk_target_list_add_text_targets (target_list, 0); context = gtk_drag_begin (GTK_WIDGET (text_view), target_list, (GdkDragAction) (GDK_ACTION_COPY | GDK_ACTION_MOVE), 1, (GdkEvent*)event); gtk_target_list_unref (target_list); gtk_drag_set_icon_default (context); } inline static int char_class (const GtkTextIter *iter) { if (gtk_text_iter_ends_line (iter)) return -1; else if (is_space (iter)) return 0; else if (is_word_char (iter)) return 1; else return 2; } int _moo_edit_extend_selection (MooEdit *edit, MooEditSelectionType type, GtkTextIter *start, GtkTextIter *end) { int order = gtk_text_iter_compare (start, end); if (type == MOO_EDIT_SELECT_CHARS) { return order; } else if (type == MOO_EDIT_SELECT_WORDS) { int ch_class; if (!order && edit->priv->double_click_selects_brackets) { GtkTextIter rstart = *start; if (moo_edit_at_bracket (get_buf (edit), &rstart) && !(order == 1 && gtk_text_iter_compare (&rstart, start) == -1)) /* this means (...)| */ { GtkTextIter rend = rstart; if (moo_edit_find_matching_bracket (get_buf (edit), &rend) == MOO_EDIT_BRACKET_MATCH_CORRECT) { if (gtk_text_iter_compare (&rstart, &rend) > 0) { /* ( ) */ if (edit->priv->double_click_selects_inside) gtk_text_iter_forward_char (&rend); else gtk_text_iter_forward_char (&rstart); } else { /* ( ) */ if (edit->priv->double_click_selects_inside) gtk_text_iter_forward_char (&rstart); else gtk_text_iter_forward_char (&rend); } *start = rstart; *end = rend; return TRUE; } } } if (order > 0) { GtkTextIter *tmp = start; start = end; end = tmp; } ch_class = char_class (end); while (!gtk_text_iter_ends_line (end) && char_class (end) == ch_class) { gtk_text_iter_forward_char (end); } ch_class = char_class (start); while (!gtk_text_iter_starts_line (start) && char_class (start) == ch_class) { gtk_text_iter_backward_char (start); } if (char_class (start) != ch_class) gtk_text_iter_forward_char (start); return gtk_text_iter_compare (start, end); } else if (type == MOO_EDIT_SELECT_LINES) { if (order > 0) { GtkTextIter *tmp = start; start = end; end = tmp; } gtk_text_iter_set_line_offset (start, 0); gtk_text_iter_forward_line (end); return gtk_text_iter_compare (start, end); } g_assert_not_reached (); } static void start_drag_scroll (MooEdit *edit) { if (!edit->priv->drag_scroll_timeout) edit->priv->drag_scroll_timeout = g_timeout_add (SCROLL_TIMEOUT, (GSourceFunc)drag_scroll_timeout_func, edit); drag_scroll_timeout_func (edit); } static void stop_drag_scroll (MooEdit *edit) { if (edit->priv->drag_scroll_timeout) g_source_remove (edit->priv->drag_scroll_timeout); edit->priv->drag_scroll_timeout = 0; } static gboolean drag_scroll_timeout_func (MooEdit *edit) { GtkTextView *text_view; int x, y, px, py; GtkTextIter iter; GtkTextIter start; MooEditSelectionType t; GtkTextBuffer *buffer; g_assert (edit->priv->drag_type == MOO_EDIT_DRAG_SELECT); text_view = GTK_TEXT_VIEW (edit); gdk_window_get_pointer (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), &px, &py, NULL); gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_TEXT, px, py, &x, &y); gtk_text_view_get_iter_at_location (text_view, &iter, x, y); buffer = gtk_text_view_get_buffer (text_view); gtk_text_view_get_iter_at_location (text_view, &start, edit->priv->drag_start_x, edit->priv->drag_start_y); if (edit->priv->drag_button == GDK_BUTTON_PRESS) t = MOO_EDIT_SELECT_CHARS; else if (edit->priv->drag_button == GDK_2BUTTON_PRESS) t = MOO_EDIT_SELECT_WORDS; else t = MOO_EDIT_SELECT_LINES; if (extend_selection (edit, t, &start, &iter)) gtk_text_buffer_select_range (buffer, &iter, &start); else gtk_text_buffer_place_cursor (buffer, &iter); gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_insert (buffer)); return TRUE; }