medit/moo/mooterm/mooterm-text.c

951 lines
25 KiB
C

/*
* mooterm/mootermselection.c
*
* Copyright (C) 2004-2005 by Yevgen Muntyan <muntyan@math.tamu.edu>
*
* 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 MOOTERM_COMPILATION
#include "mooterm/mooterm-private.h"
#include "mooterm/mootermbuffer-private.h"
#include "mooterm/mooterm-selection.h"
#include "mooui/mootext.h"
typedef struct {
GtkTextIter start;
GtkTextIter end;
} Segment;
#define GET_SELECTION(term) ((Segment*)(term)->priv->selection)
#define ITER_ROW(iter) ((iter)->dummy3)
#define ITER_COL(iter) ((iter)->dummy4)
#define ITER_WIDTH(iter) ((iter)->dummy5)
#define ITER_TERM(iter) ((MooTerm*)(iter)->dummy1)
#define ITER_SET_TERM(iter, term) (iter)->dummy1 = term
#define ITER_BUF(iter) (ITER_TERM(iter)->priv->buffer)
#define ITER_TOTAL_HEIGHT(iter) ((int)(ITER_TERM(iter)->priv->height + \
buf_scrollback (ITER_BUF(iter))))
#define FILL_ITER(iter, term, row, col) \
(iter)->dummy1 = term; \
ITER_ROW(iter) = row; \
ITER_COL(iter) = col; \
ITER_WIDTH(iter) = term->priv->width; \
CHECK_ITER(iter);
#ifdef DEBUG
static void CHECK_ITER (const GtkTextIter *iter)
{
g_assert (ITER_TERM(iter) != NULL);
g_assert ((int)ITER_TERM(iter)->priv->width == ITER_WIDTH(iter));
g_assert (ITER_ROW(iter) >= 0);
g_assert (ITER_ROW(iter) < ITER_TOTAL_HEIGHT(iter));
g_assert (ITER_COL(iter) >= 0);
g_assert (ITER_COL(iter) <= ITER_WIDTH(iter));
}
static void CHECK_SEGMENT (const Segment *segment)
{
CHECK_ITER (&segment->start);
CHECK_ITER (&segment->end);
}
#else
#define CHECK_ITER(iter)
#define CHECK_SEGMENT(segment)
#endif
static void get_start_iter (MooTerm *term,
GtkTextIter *iter);
static void get_end_iter (MooTerm *term,
GtkTextIter *iter);
static int iter_cmp (const GtkTextIter *first,
const GtkTextIter *second);
static void iter_order (GtkTextIter *first,
GtkTextIter *second);
static void iter_set_start (GtkTextIter *iter);
static char *segment_get_text (Segment *segment);
static void invalidate_segment (Segment *segments,
guint num);
gpointer moo_term_selection_new (MooTerm *term)
{
Segment *sel = g_new0 (Segment, 1);
FILL_ITER (&sel->start, term, 0, 0);
FILL_ITER (&sel->end, term, 0, 0);
return sel;
}
void moo_term_selection_invalidate (MooTerm *term)
{
Segment *sel = term->priv->selection;
invalidate_segment (sel, 1);
FILL_ITER (&sel->start, term, 0, 0);
FILL_ITER (&sel->end, term, 0, 0);
moo_term_release_selection (term);
}
static gboolean segment_empty (Segment *s)
{
CHECK_SEGMENT (s);
return !iter_cmp (&s->start, &s->end);
}
static int segment_sym_diff (Segment *s1, Segment *s2,
Segment *result)
{
Segment *left = result;
Segment *right = &result[1];
CHECK_SEGMENT (s1);
CHECK_SEGMENT (s2);
if (segment_empty (s1))
{
if (segment_empty (s2))
{
return 0;
}
else
{
*left = *s2;
CHECK_SEGMENT (left);
return 1;
}
}
else if (segment_empty (s2))
{
*left = *s1;
CHECK_SEGMENT (left);
return 1;
}
else
{
Segment tmp;
GtkTextIter itmp;
*left = *s1;
*right = *s2;
iter_order (&left->start, &left->end);
iter_order (&right->start, &right->end);
switch (iter_cmp (&left->start, &right->start))
{
case 0:
switch (iter_cmp (&left->end, &right->end))
{
case 0:
return 0;
case 1:
left->start = right->end;
CHECK_SEGMENT (left);
return 1;
case -1:
left->start = left->end;
left->end = right->end;
CHECK_SEGMENT (left);
return 1;
}
case 1:
tmp = *left;
*left = *right;
*right = tmp;
break;
}
switch (iter_cmp (&left->end, &right->start))
{
case 0:
left->end = right->end;
CHECK_SEGMENT (left);
return 1;
case -1:
CHECK_SEGMENT (left);
CHECK_SEGMENT (right);
return 2;
case 1:
itmp = left->end;
left->end = right->start;
right->start = itmp;
CHECK_SEGMENT (left);
CHECK_SEGMENT (right);
return 2;
}
}
g_return_val_if_reached (0);
}
static void invalidate_segment (Segment *segm, guint num)
{
MooTerm *term;
int top_line;
guint i;
if (!num)
return;
term = ITER_TERM (&segm->start);
top_line = term_top_line (term);
for (i = 0; i < num; ++i)
{
GtkTextIter start = segm[i].start;
GtkTextIter end = segm[i].end;
GdkRectangle rect;
iter_order (&start, &end);
if (ITER_ROW (&start) < ITER_ROW (&end))
{
if (ITER_COL (&start) < (int)term->priv->width)
{
rect.x = ITER_COL (&start);
rect.width = term->priv->width - rect.x;
rect.y = ITER_ROW (&start) - top_line;
rect.height = 1;
moo_term_invalidate_rect (term, &rect);
}
if (ITER_ROW (&start) + 1 < ITER_ROW (&end))
{
rect.x = 0;
rect.width = term->priv->width;
rect.y = ITER_ROW (&start) + 1 - top_line;
rect.height = ITER_ROW (&end) - ITER_ROW (&start) - 1;
moo_term_invalidate_rect (term, &rect);
}
if (ITER_COL (&end) > 0)
{
rect.x = 0;
rect.width = ITER_COL (&end);
rect.y = ITER_ROW (&end) - top_line;
rect.height = 1;
moo_term_invalidate_rect (term, &rect);
}
}
else
{
if (ITER_COL (&start) < ITER_COL (&end))
{
rect.x = ITER_COL (&start);
rect.width = ITER_COL (&end) - ITER_COL (&start);
rect.y = ITER_ROW (&start) - top_line;
rect.height = 1;
moo_term_invalidate_rect (term, &rect);
}
}
}
}
static void term_select_range (const GtkTextIter *start,
const GtkTextIter *end)
{
Segment diff[2];
Segment new_sel;
Segment old_selection;
gboolean inv = FALSE;
MooTerm *term = ITER_TERM (start);
CHECK_ITER (start);
CHECK_ITER (end);
old_selection = *GET_SELECTION (ITER_TERM (start));
CHECK_SEGMENT (&old_selection);
new_sel.start = *start;
new_sel.end = *end;
switch (iter_cmp (start, end))
{
case 0:
iter_set_start (&new_sel.start);
iter_set_start (&new_sel.end);
break;
case 1:
inv = TRUE;
break;
}
iter_order (&new_sel.start, &new_sel.end);
if (ITER_COL(&new_sel.start) == ITER_WIDTH(&new_sel.start))
{
g_assert (ITER_ROW(&new_sel.start) < ITER_TOTAL_HEIGHT(&new_sel.start));
if (ITER_ROW(&new_sel.start) + 1 < ITER_TOTAL_HEIGHT(&new_sel.start))
{
ITER_ROW(&new_sel.start)++;
ITER_COL(&new_sel.start) = 0;
}
}
if (ITER_COL(&new_sel.end) == 0 && ITER_ROW(&new_sel.end) != 0)
{
ITER_ROW(&new_sel.end)--;
ITER_COL(&new_sel.end) = ITER_WIDTH(&new_sel.start);
}
if (inv)
iter_order (&new_sel.end, &new_sel.start);
CHECK_SEGMENT (&new_sel);
*GET_SELECTION (ITER_TERM (start)) = new_sel;
invalidate_segment (diff,
segment_sym_diff (&new_sel,
&old_selection,
diff));
if (moo_term_selection_empty (term))
moo_term_release_selection (term);
else
moo_term_grab_selection (term);
}
static void get_start_iter (MooTerm *term,
GtkTextIter *iter)
{
FILL_ITER (iter, term, 0, 0);
}
static void get_end_iter (MooTerm *term,
GtkTextIter *iter)
{
int width = term->priv->width;
int total_height = term->priv->height + buf_scrollback (term->priv->buffer);
FILL_ITER (iter, term, total_height - 1, width);
}
gboolean moo_term_button_press (GtkWidget *widget,
GdkEventButton *event)
{
moo_term_set_pointer_visible (MOO_TERM (widget), TRUE);
return moo_text_button_press_event (widget, event);
}
gboolean moo_term_button_release (GtkWidget *widget,
GdkEventButton *event)
{
moo_term_set_pointer_visible (MOO_TERM (widget), TRUE);
return moo_text_button_release_event (widget, event);
}
gboolean moo_term_motion_notify (GtkWidget *widget,
GdkEventMotion *event)
{
moo_term_set_pointer_visible (MOO_TERM (widget), TRUE);
return moo_text_motion_event (widget, event);
}
gboolean moo_term_selection_empty (MooTerm *term)
{
return segment_empty (GET_SELECTION (term));
}
gboolean moo_term_get_selection_bounds (MooTerm *term,
guint *left_row,
guint *left_col,
guint *right_row,
guint *right_col)
{
Segment *selection = GET_SELECTION (term);
if (segment_empty (selection))
{
return FALSE;
}
else
{
GtkTextIter start = selection->start;
GtkTextIter end = selection->end;
iter_order (&start, &end);
if (left_row)
*left_row = ITER_ROW (&start);
if (left_col)
*left_col = ITER_COL (&start);
if (right_row)
*right_row = ITER_ROW (&end);
if (right_col)
*right_col = ITER_COL (&end);
return TRUE;
}
}
void moo_term_selection_clear (MooTerm *term)
{
GtkTextIter i;
FILL_ITER (&i, term, 0, 0);
term_select_range (&i, &i);
}
static void middle_button_click (MooText *obj,
G_GNUC_UNUSED GdkEventButton *event)
{
moo_term_paste_clipboard (MOO_TERM (obj), GDK_SELECTION_PRIMARY);
}
static void right_button_click (MooText *obj,
GdkEventButton *event)
{
moo_term_do_popup_menu (MOO_TERM (obj), event);
}
static void window_to_buffer_coords (MooText *obj,
int window_x,
int window_y,
int *buffer_x,
int *buffer_y)
{
MooTerm *term = MOO_TERM (obj);
*buffer_x = window_x;
*buffer_y = window_y + (term_top_line (term) * term_char_height (term));
}
static void get_iter_at_location (MooText *obj,
GtkTextIter *iter,
int x,
int y)
{
MooTerm *term = MOO_TERM (obj);
int char_width = term_char_width (term);
int char_height = term_char_height (term);
int scrollback = buf_scrollback (term->priv->buffer);
g_return_if_fail (iter != NULL);
y /= char_height;
x = CLAMP (x, 0, (int)term->priv->width * char_width - 1);
x = (x % char_width > char_width / 2) ?
x / char_width + 1 :
x / char_width;
if (y < 0)
{
y = 0;
x = 0;
}
else if (y > scrollback + (int)term->priv->height - 1)
{
y = scrollback + (int)term->priv->height - 1;
x = term->priv->width;
}
FILL_ITER (iter, term, y, x);
}
static gboolean get_selection_bounds (MooText *obj,
GtkTextIter *sel_start,
GtkTextIter *sel_end)
{
MooTerm *term = MOO_TERM (obj);
Segment *selection = term->priv->selection;
if (sel_start)
*sel_start = selection->start;
if (sel_end)
*sel_end = selection->end;
return !moo_term_selection_empty (term);
}
static int iter_cmp (const GtkTextIter *first,
const GtkTextIter *second)
{
g_assert (ITER_TERM (first) == ITER_TERM (second));
if (ITER_ROW (first) < ITER_ROW (second))
return -1;
else if (ITER_ROW (first) > ITER_ROW (second))
return 1;
else if (ITER_COL (first) < ITER_COL (second))
return -1;
else if (ITER_COL (first) > ITER_COL (second))
return 1;
else
return 0;
}
static void iter_set_start (GtkTextIter *iter)
{
ITER_ROW (iter) = ITER_COL (iter) = 0;
CHECK_ITER (iter);
}
static void iter_order (GtkTextIter *first,
GtkTextIter *second)
{
if (iter_cmp (first, second) > 0)
{
GtkTextIter tmp = *first;
*first = *second;
*second = tmp;
}
}
static gboolean iter_in_range (const GtkTextIter *iter,
const GtkTextIter *start,
const GtkTextIter *end)
{
CHECK_ITER (iter);
CHECK_ITER (start);
CHECK_ITER (end);
return iter_cmp (start, iter) <= 0 && iter_cmp (iter, end) <= 0;
}
static void select_range (G_GNUC_UNUSED MooText *obj,
const GtkTextIter *start,
const GtkTextIter *end)
{
CHECK_ITER (start);
CHECK_ITER (end);
term_select_range (start, end);
}
static void place_selection_end (MooText *obj,
const GtkTextIter *where)
{
MooTerm *term = MOO_TERM (obj);
CHECK_ITER (where);
term_select_range (&GET_SELECTION(term)->start, where);
}
static void scroll_selection_end_onscreen (MooText *obj)
{
MooTerm *term = MOO_TERM (obj);
GtkTextIter iter = GET_SELECTION(term)->end;
guint top_line = term_top_line (term);
if (ITER_ROW(&iter) < (int)top_line)
moo_term_scroll_lines (term, ITER_ROW(&iter) - (int)top_line);
else if (ITER_ROW(&iter) >= (int)top_line + (int)term->priv->height)
moo_term_scroll_lines (term, ITER_ROW(&iter) + 1 -
(int)top_line - (int)term->priv->height);
}
static GdkWindow* get_window (MooText *obj)
{
return GTK_WIDGET(obj)->window;
}
static void get_visible_rect (MooText *obj,
GdkRectangle *rect)
{
MooTerm *term = MOO_TERM (obj);
int char_height = term_char_height (term);
rect->x = 0;
rect->width = term->priv->width * term_char_width (term);
rect->y = term_top_line (term) * char_height;
rect->height = term->priv->height * char_height;
}
gboolean moo_term_cell_selected (MooTerm *term,
guint row,
guint col)
{
GtkTextIter start, end, iter;
if (get_selection_bounds (MOO_TEXT (term), &start, &end))
{
iter_order (&start, &end);
FILL_ITER (&iter, term, row, col);
return iter_cmp (&start, &iter) <= 0 && iter_cmp (&iter, &end) < 0;
}
else
{
return FALSE;
}
}
int moo_term_row_selected (MooTerm *term,
guint row)
{
guint l_row, l_col, r_row, r_col;
if (moo_term_get_selection_bounds (term, &l_row, &l_col,
&r_row, &r_col))
{
if (row < l_row || row > r_row)
return NOT_SELECTED;
else if (l_row < row && row < r_row)
return FULL_SELECTED;
else
return PART_SELECTED;
}
else
{
return NOT_SELECTED;
}
}
static gboolean extend_selection (MooText *obj,
MooTextSelectionType type,
GtkTextIter *end,
GtkTextIter *start);
void moo_term_text_iface_init (gpointer g_iface)
{
MooTextIface *iface = (MooTextIface*) g_iface;
iface->start_selection_dnd = NULL;
iface->middle_button_click = middle_button_click;
iface->right_button_click = right_button_click;
iface->extend_selection = extend_selection;
iface->window_to_buffer_coords = window_to_buffer_coords;
iface->get_iter_at_location = get_iter_at_location;
iface->get_selection_bounds = get_selection_bounds;
iface->iter_order = iter_order;
iface->iter_in_range = iter_in_range;
iface->place_selection_end = place_selection_end;
iface->select_range = select_range;
iface->scroll_selection_end_onscreen = scroll_selection_end_onscreen;
iface->get_window = get_window;
iface->get_visible_rect = get_visible_rect;
}
char *moo_term_get_selection (MooTerm *term)
{
return segment_get_text (term->priv->selection);
}
static int char_class (MooTerm *term,
const GtkTextIter *iter);
static gboolean iter_ends_line (const GtkTextIter *iter);
static gboolean iter_starts_line (const GtkTextIter *iter);
static void iter_forward_char (GtkTextIter *iter);
static void iter_backward_char (GtkTextIter *iter);
static void iter_set_line_offset (GtkTextIter *iter,
guint offset);
static gboolean iter_forward_line (GtkTextIter *iter);
static gboolean is_space (const GtkTextIter *iter);
static gboolean is_word_char (const GtkTextIter *iter);
static gunichar iter_get_char (const GtkTextIter *iter);
static gboolean extend_selection (MooText *obj,
MooTextSelectionType type,
GtkTextIter *end,
GtkTextIter *start)
{
MooTerm *term = MOO_TERM (obj);
int order = iter_cmp (start, end);
CHECK_ITER (start);
CHECK_ITER (end);
if (type == MOO_TEXT_SELECT_CHARS)
{
return order;
}
if (order > 0)
{
GtkTextIter *tmp = start;
start = end; end = tmp;
}
if (type == MOO_TEXT_SELECT_WORDS)
{
int ch_class;
ch_class = char_class (term, end);
while (!iter_ends_line (end) &&
char_class (term, end) == ch_class)
{
iter_forward_char (end);
}
ch_class = char_class (term, start);
while (!iter_starts_line (start) &&
char_class (term, start) == ch_class)
{
iter_backward_char (start);
}
if (char_class (term, start) != ch_class)
iter_forward_char (start);
return iter_cmp (start, end);
}
if (type == MOO_TEXT_SELECT_LINES)
{
iter_set_line_offset (start, 0);
iter_forward_line (end);
return iter_cmp (start, end);
}
g_return_val_if_reached (FALSE);
}
/* TODO */
static int char_class (MooTerm *term,
const GtkTextIter *iter)
{
if (iter_ends_line (iter))
return -1;
else if (is_space (iter))
return 0;
else if (is_word_char (iter))
return 1;
else
return 2;
}
static gboolean iter_ends_line (const GtkTextIter *iter)
{
return ITER_COL(iter) == (int)ITER_TERM(iter)->priv->width;
}
static gboolean iter_starts_line (const GtkTextIter *iter)
{
CHECK_ITER (iter);
return ITER_COL(iter) == 0;
}
static void iter_forward_char (GtkTextIter *iter)
{
MooTerm *term = ITER_TERM(iter);
int width = term->priv->width;
int total_height = term->priv->height + buf_scrollback (term->priv->buffer);
g_return_if_fail (ITER_ROW(iter) < total_height || ITER_COL(iter) < width);
if (ITER_COL(iter) < width)
{
ITER_COL(iter)++;
}
else
{
ITER_COL(iter) = 0;
ITER_ROW(iter)++;
}
CHECK_ITER (iter);
}
static void iter_backward_char (GtkTextIter *iter)
{
MooTerm *term = ITER_TERM(iter);
int width = term->priv->width;
g_return_if_fail (ITER_ROW(iter) > 0 || ITER_COL(iter) > 0);
if (ITER_COL(iter) > 0)
{
ITER_COL(iter)--;
}
else
{
ITER_ROW(iter)--;
ITER_COL(iter) = width;
}
CHECK_ITER (iter);
}
static void iter_set_line_offset (GtkTextIter *iter,
guint offset)
{
g_return_if_fail (offset <= ITER_TERM(iter)->priv->width);
ITER_COL(iter) = offset;
CHECK_ITER (iter);
}
static gboolean iter_forward_line (GtkTextIter *iter)
{
MooTerm *term = ITER_TERM(iter);
int width = term->priv->width;
int total_height = term->priv->height + buf_scrollback (term->priv->buffer);
if (ITER_ROW(iter) == total_height - 1)
{
ITER_COL(iter) = width;
CHECK_ITER (iter);
return FALSE;
}
else
{
ITER_COL(iter) = 0;
ITER_ROW(iter)++;
CHECK_ITER (iter);
return TRUE;
}
}
static gboolean is_space (const GtkTextIter *iter)
{
gunichar c = iter_get_char (iter);
g_return_val_if_fail (c != 0, FALSE);
return g_unichar_isspace (c);
}
static gboolean is_word_char (const GtkTextIter *iter)
{
gunichar c = iter_get_char (iter);
g_return_val_if_fail (c != 0, FALSE);
return g_unichar_isalnum (c) || c == '_';
}
static gunichar iter_get_char (const GtkTextIter *iter)
{
MooTermLine *line = buf_line (ITER_TERM(iter)->priv->buffer,
ITER_ROW(iter));
return moo_term_line_get_unichar (line, ITER_COL(iter));
}
static char *segment_get_text (Segment *segment)
{
MooTerm *term = ITER_TERM (&segment->start);
MooTermBuffer *buf = term->priv->buffer;
GtkTextIter start = segment->start;
GtkTextIter end = segment->end;
GString *text;
MooTermLine *line;
int width = term->priv->width;
int i;
iter_order (&start, &end);
if (!iter_cmp (&start, &end))
return NULL;
text = g_string_new ("");
if (ITER_ROW(&start) == ITER_ROW(&end))
{
line = buf_line (buf, ITER_ROW(&start));
for (i = ITER_COL(&start); i < ITER_COL(&end); ++i)
g_string_append_unichar (text, moo_term_line_get_unichar (line, i));
}
else
{
if (ITER_COL(&start) < width)
{
line = buf_line (buf, ITER_ROW(&start));
for (i = ITER_COL(&start); i < width; ++i)
g_string_append_unichar (text, moo_term_line_get_unichar (line, i));
g_string_append_c (text, '\n');
}
if (ITER_ROW(&start) + 1 < ITER_ROW(&end))
{
for (i = ITER_ROW(&start) + 1; i < ITER_ROW(&end); ++i)
{
int j;
line = buf_line (buf, i);
for (j = 0; j < width; ++j)
g_string_append_unichar (text, moo_term_line_get_unichar (line, j));
g_string_append_c (text, '\n');
}
}
if (ITER_COL(&end) > 0)
{
line = buf_line (buf, ITER_ROW(&end));
for (i = 0; i < ITER_COL(&start); ++i)
g_string_append_unichar (text, moo_term_line_get_unichar (line, i));
}
}
return g_string_free (text, FALSE);
}
void moo_term_select_all (MooTerm *term)
{
GtkTextIter start, end;
get_start_iter (term, &start);
get_end_iter (term, &end);
term_select_range (&start, &end);
}
char *moo_term_get_content (MooTerm *term)
{
Segment segm;
get_start_iter (term, &segm.start);
get_end_iter (term, &segm.end);
return segment_get_text (&segm);
}