refixed buffer overflow
[claws.git] / src / undo.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2001 Hiroyuki Yamamoto
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 /* code ported from gedit */
21 /* This is for my patient girlfirend Regina */
22
23 #ifdef HAVE_CONFIG_H
24 #  include "config.h"
25 #endif
26
27 #include <glib.h>
28
29 #include <string.h> /* for strlen */
30 #include <stdlib.h> /* for mbstowcs */
31
32 #include "undo.h"
33 #include "utils.h"
34 #include "prefs_common.h"
35 #include "gtkstext.h"
36
37 typedef struct _UndoInfo UndoInfo;
38
39 struct _UndoInfo 
40 {
41         UndoAction action;
42         gchar *text;
43         gint start_pos;
44         gint end_pos;
45         gfloat window_position;
46         gint mergeable;
47 };
48
49 static void undo_free_list      (GList         **list_pointer);
50 static void undo_check_size     (UndoMain       *undostruct);
51 static gint undo_merge          (GList          *list,
52                                  guint           start_pos,
53                                  guint           end_pos,
54                                  gint            action,
55                                  const guchar   *text);
56 static void undo_add            (const gchar    *text,
57                                  gint            start_pos,
58                                  gint            end_pos,
59                                  UndoAction      action,
60                                  UndoMain       *undostruct);
61 static gint undo_get_selection  (GtkEditable    *text,
62                                  guint          *start,
63                                  guint          *end);
64 static void undo_insert_text_cb (GtkEditable    *editable,
65                                  gchar          *new_text,
66                                  gint            new_text_length,
67                                  gint           *position,
68                                  UndoMain       *undostruct);
69 static void undo_delete_text_cb (GtkEditable    *editable,
70                                  gint            start_pos,
71                                  gint            end_pos,
72                                  UndoMain       *undostruct);
73
74 static void undo_paste_clipboard_cb     (GtkEditable    *editable,
75                                          UndoMain       *undostruct);
76
77 void undo_undo                  (UndoMain       *undostruct);
78 void undo_redo                  (UndoMain       *undostruct);
79
80
81 UndoMain *undo_init(GtkWidget *text) 
82 {
83         UndoMain *undostruct;
84         
85         g_return_val_if_fail(text != NULL, NULL);
86
87         undostruct = g_new(UndoMain, 1);
88         undostruct->text = text;
89         undostruct->undo = NULL;
90         undostruct->redo = NULL;
91         undostruct->paste = 0;
92         undostruct->undo_state = FALSE;
93         undostruct->redo_state = FALSE;
94
95         gtk_signal_connect(GTK_OBJECT(text), "insert-text",
96                            GTK_SIGNAL_FUNC(undo_insert_text_cb), undostruct);
97         gtk_signal_connect(GTK_OBJECT(text), "delete-text",
98                            GTK_SIGNAL_FUNC(undo_delete_text_cb), undostruct);
99         gtk_signal_connect(GTK_OBJECT(text), "paste-clipboard",
100                            GTK_SIGNAL_FUNC(undo_paste_clipboard_cb), undostruct);
101
102         return undostruct;
103 }
104
105 void undo_destroy (UndoMain *undostruct) 
106 {
107         undo_free_list(&undostruct->undo);
108         undo_free_list(&undostruct->redo);
109         g_free(undostruct);
110 }
111
112 static UndoInfo *undo_object_new(gchar *text, gint start_pos, gint end_pos, 
113                                  UndoAction action, gfloat window_position) 
114 {
115         UndoInfo *undoinfo;
116         undoinfo = g_new (UndoInfo, 1);
117         undoinfo->text      = text;
118         undoinfo->start_pos = start_pos;
119         undoinfo->end_pos   = end_pos;
120         undoinfo->action    = action;
121         undoinfo->window_position = window_position;
122         return undoinfo;
123 }
124
125 static void undo_object_free(UndoInfo *undo) 
126 {
127         g_free (undo->text);
128         g_free (undo);
129 }
130
131 /**
132  * undo_free_list:
133  * @list_pointer: list to be freed
134  *
135  * frees and undo structure list
136  **/
137 static void undo_free_list(GList **list_pointer) 
138 {
139         UndoInfo *undo;
140         GList *cur, *list = *list_pointer;
141
142         if (list == NULL) return;
143
144         for (cur = list; cur != NULL; cur = cur->next) {
145                 undo = (UndoInfo *)cur->data;
146                 undo_object_free(undo);
147         }
148
149         g_list_free(list);
150         *list_pointer = NULL;
151 }
152
153 void undo_set_change_state_func(UndoMain *undostruct, UndoChangeStateFunc func,
154                                 gpointer data)
155 {
156         g_return_if_fail(undostruct != NULL);
157
158         undostruct->change_state_func = func;
159         undostruct->change_state_data = data;
160 }
161
162 /**
163  * undo_check_size:
164  * @compose: document to check
165  *
166  * Checks that the size of compose->undo does not excede settings->undo_levels and
167  * frees any undo level above sett->undo_level.
168  *
169  **/
170 static void undo_check_size(UndoMain *undostruct) 
171 {
172         UndoInfo *last_undo;
173         guint length;
174
175         if (prefs_common.undolevels < 1) return;
176
177         /* No need to check for the redo list size since the undo
178            list gets freed on any call to compose_undo_add */
179         length = g_list_length(undostruct->undo);
180         if (length >= prefs_common.undolevels && prefs_common.undolevels > 0) {
181                 last_undo = (UndoInfo *)g_list_last(undostruct->undo)->data;
182                 undostruct->undo = g_list_remove(undostruct->undo, last_undo);
183                 undo_object_free(last_undo);
184         }
185 }
186
187 /**
188  * undo_merge:
189  * @last_undo:
190  * @start_pos:
191  * @end_pos:
192  * @action:
193  *
194  * This function tries to merge the undo object at the top of
195  * the stack with a new set of data. So when we undo for example
196  * typing, we can undo the whole word and not each letter by itself
197  *
198  * Return Value: TRUE is merge was sucessful, FALSE otherwise
199  **/
200 static gint undo_merge(GList *list, guint start_pos, guint end_pos,
201                        gint action, const guchar *text) 
202 {
203         guchar *temp_string;
204         UndoInfo *last_undo;
205
206         /* This are the cases in which we will NOT merge :
207            1. if (last_undo->mergeable == FALSE)
208            [mergeable = FALSE when the size of the undo data was not 1.
209            or if the data was size = 1 but = '\n' or if the undo object
210            has been "undone" already ]
211            2. The size of text is not 1
212            3. If the new merging data is a '\n'
213            4. If the last char of the undo_last data is a space/tab
214            and the new char is not a space/tab ( so that we undo
215            words and not chars )
216            5. If the type (action) of undo is different from the last one
217            Chema */
218
219         if (list == NULL) return FALSE;
220
221         last_undo = list->data;
222
223         if (!last_undo->mergeable) return FALSE;
224
225         if (end_pos - start_pos != 1 ||
226             text[0] == '\n' ||
227             action != last_undo->action ||
228             action == UNDO_ACTION_REPLACE_INSERT ||
229             action == UNDO_ACTION_REPLACE_DELETE) {
230                 last_undo->mergeable = FALSE;
231                 return FALSE;
232         }
233
234         if (action == UNDO_ACTION_DELETE) {
235                 gboolean checkit = TRUE;
236
237                 if (last_undo->start_pos != end_pos &&
238                     last_undo->start_pos != start_pos) {
239                         last_undo->mergeable = FALSE;
240                         return FALSE;
241                 } else if (last_undo->start_pos == start_pos) {
242                         /* Deleted with the delete key */
243                         if (text[0] != ' ' && text[0] != '\t' &&
244                             (last_undo->text[last_undo->end_pos - last_undo->start_pos - 1] == ' ' ||
245                              last_undo->text[last_undo->end_pos - last_undo->start_pos - 1] == '\t'))
246                                 checkit = FALSE;
247
248                         temp_string = g_strdup_printf("%s%s", last_undo->text, text);
249                         last_undo->end_pos++;
250                         g_free(last_undo->text);
251                         last_undo->text = temp_string;
252                 } else {
253                         /* Deleted with the backspace key */
254                         if (text[0] != ' ' && text[0] != '\t' &&
255                             (last_undo->text[0] == ' ' ||
256                              last_undo->text[0] == '\t'))
257                                 checkit = FALSE;
258
259                         temp_string = g_strdup_printf("%s%s", text, last_undo->text);
260                         last_undo->start_pos = start_pos;
261                         g_free(last_undo->text);
262                         last_undo->text = temp_string;
263                 }
264
265                 if (!checkit) {
266                         last_undo->mergeable = FALSE;
267                         return FALSE;
268                 }
269         } else if (action == UNDO_ACTION_INSERT) {
270                 if (last_undo->end_pos != start_pos) {
271                         last_undo->mergeable = FALSE;
272                         return FALSE;
273                 } else {
274                         temp_string = g_strdup_printf("%s%s", last_undo->text, text);
275                         g_free(last_undo->text);
276                         last_undo->end_pos = end_pos;
277                         last_undo->text = temp_string;
278                 }
279         } else
280                 debug_print("Unknown action [%i] inside undo merge encountered", action);
281
282         return TRUE;
283 }
284
285 /**
286  * compose_undo_add:
287  * @text:
288  * @start_pos:
289  * @end_pos:
290  * @action: either UNDO_ACTION_INSERT or UNDO_ACTION_DELETE
291  * @compose:
292  * @view: The view so that we save the scroll bar position.
293  *
294  * Adds text to the undo stack. It also performs test to limit the number
295  * of undo levels and deltes the redo list
296  **/
297
298 static void undo_add(const gchar *text, 
299                      gint start_pos, gint end_pos,
300                      UndoAction action, UndoMain *undostruct) 
301 {
302         UndoInfo *undoinfo;
303
304         g_return_if_fail(text != NULL);
305         g_return_if_fail(end_pos >= start_pos);
306
307         undo_free_list(&undostruct->redo);
308
309         /* Set the redo sensitivity */
310         undostruct->change_state_func(undostruct,
311                                       UNDO_STATE_UNCHANGED, UNDO_STATE_FALSE,
312                                       undostruct->change_state_data);
313
314         if (undostruct->paste != 0) {
315                 if (action == UNDO_ACTION_INSERT) 
316                         action = UNDO_ACTION_REPLACE_INSERT;
317                 else 
318                         action = UNDO_ACTION_REPLACE_DELETE;
319                 undostruct->paste = undostruct->paste + 1;
320                 if (undostruct->paste == 3) 
321                         undostruct->paste = 0;
322         }
323
324         if (undo_merge(undostruct->undo, start_pos, end_pos, action, text))
325                 return;
326
327         undo_check_size(undostruct);
328
329         undoinfo = undo_object_new(g_strdup(text), start_pos, end_pos, action,
330                                    GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj)->value);
331
332         if (end_pos - start_pos != 1 || text[0] == '\n')
333                 undoinfo->mergeable = FALSE;
334         else
335                 undoinfo->mergeable = TRUE;
336
337         undostruct->undo = g_list_prepend(undostruct->undo, undoinfo);
338
339         undostruct->change_state_func(undostruct,
340                                       UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED,
341                                       undostruct->change_state_data);
342 }
343
344 /**
345  * undo_undo:
346  * @w: not used
347  * @data: not used
348  *
349  * Executes an undo request on the current document
350  **/
351 void undo_undo(UndoMain *undostruct) 
352 {
353         UndoInfo *undoinfo;
354         guint start_pos, end_pos;
355
356         g_return_if_fail(undostruct != NULL);
357
358         if (undostruct->undo == NULL) return;
359
360         /* The undo data we need is always at the top op the
361            stack. So, therefore, the first one */
362         undoinfo = (UndoInfo *)undostruct->undo->data;
363         g_return_if_fail(undoinfo != NULL);
364         undoinfo->mergeable = FALSE;
365         undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
366         undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
367
368         /* Check if there is a selection active */
369         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
370         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
371         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
372                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text),
373                                            0, 0);
374
375         /* Move the view (scrollbars) to the correct position */
376         gtk_adjustment_set_value
377                 (GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj),
378                  undoinfo->window_position);
379
380         switch (undoinfo->action) {
381         case UNDO_ACTION_DELETE:
382                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
383                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
384                 break;
385         case UNDO_ACTION_INSERT:
386                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
387                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
388                 break;
389         case UNDO_ACTION_REPLACE_INSERT:
390                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
391                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
392                 /* "pull" another data structure from the list */
393                 undoinfo = (UndoInfo *)undostruct->undo->data;
394                 g_return_if_fail(undoinfo != NULL);
395                 undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
396                 undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
397                 g_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
398                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
399                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
400                 break;
401         case UNDO_ACTION_REPLACE_DELETE:
402                 g_warning("This should not happen. UNDO_REPLACE_DELETE");
403                 break;
404         default:
405                 g_assert_not_reached();
406                 break;
407         }
408
409         undostruct->change_state_func(undostruct,
410                                       UNDO_STATE_UNCHANGED, UNDO_STATE_TRUE,
411                                       undostruct->change_state_data);
412
413         if (undostruct->undo == NULL)
414                 undostruct->change_state_func(undostruct,
415                                               UNDO_STATE_FALSE,
416                                               UNDO_STATE_UNCHANGED,
417                                               undostruct->change_state_data);
418 }
419
420 /**
421  * undo_redo:
422  * @w: not used
423  * @data: not used
424  *
425  * executes a redo request on the current document
426  **/
427 void undo_redo(UndoMain *undostruct) 
428 {
429         UndoInfo *redoinfo;
430         guint start_pos, end_pos;
431
432         g_return_if_fail(undostruct != NULL);
433
434         if (undostruct->redo == NULL) return;
435
436         redoinfo = (UndoInfo *)undostruct->redo->data;
437         g_return_if_fail (redoinfo != NULL);
438         undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
439         undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
440
441         /* Check if there is a selection active */
442         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
443         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
444         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
445                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text), 0, 0);
446
447         /* Move the view to the right position. */
448         gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj), 
449                                  redoinfo->window_position);
450
451         switch (redoinfo->action) {
452         case UNDO_ACTION_INSERT:
453                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
454                                    redoinfo->start_pos);
455                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
456                                 NULL, redoinfo->text, -1);
457                 break;
458         case UNDO_ACTION_DELETE:
459                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
460                                    redoinfo->end_pos);
461                 gtk_stext_backward_delete
462                         (GTK_STEXT(undostruct->text), 
463                          redoinfo->end_pos - redoinfo->start_pos);
464                 break;
465         case UNDO_ACTION_REPLACE_DELETE:
466                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
467                                    redoinfo->end_pos);
468                 gtk_stext_backward_delete
469                         (GTK_STEXT(undostruct->text), 
470                          redoinfo->end_pos - redoinfo->start_pos);
471                 /* "pull" another data structure from the list */
472                 redoinfo = (UndoInfo *)undostruct->redo->data;
473                 g_return_if_fail(redoinfo != NULL);
474                 undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
475                 undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
476                 g_return_if_fail(redoinfo->action == UNDO_ACTION_REPLACE_INSERT);
477                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
478                                    redoinfo->start_pos);
479                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
480                                 NULL, redoinfo->text, -1);
481                 break;
482         case UNDO_ACTION_REPLACE_INSERT:
483                 g_warning("This should not happen. Redo: UNDO_REPLACE_INSERT");
484                 break;
485         default:
486                 g_assert_not_reached();
487                 break;
488         }
489
490         undostruct->change_state_func(undostruct,
491                                       UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED, 
492                                       undostruct->change_state_data);
493
494         if (undostruct->redo == NULL)
495                 undostruct->change_state_func(undostruct,
496                                               UNDO_STATE_UNCHANGED,
497                                               UNDO_STATE_FALSE,
498                                               undostruct->change_state_data);
499 }
500
501 void undo_insert_text_cb(GtkEditable *editable, gchar *new_text,
502                          gint new_text_length, gint *position, 
503                          UndoMain *undostruct) 
504 {
505         gchar *text_to_insert;
506         size_t wlen;
507
508         if (prefs_common.undolevels <= 0) return;
509
510         Xstrndup_a(text_to_insert, new_text, new_text_length, return);
511         if (MB_CUR_MAX > 1) {
512                 wchar_t *wstr;
513
514                 wstr = g_new(wchar_t, new_text_length + 1);
515                 wlen = mbstowcs(wstr, text_to_insert, new_text_length + 1);
516                 g_free(wstr);
517                 if (wlen < 0) return;
518         } else
519                 wlen = new_text_length;
520
521         undo_add(text_to_insert, *position, *position + wlen,
522                  UNDO_ACTION_INSERT, undostruct);
523 }
524
525 void undo_delete_text_cb(GtkEditable *editable, gint start_pos,
526                          gint end_pos, UndoMain *undostruct) 
527 {
528         gchar *text_to_delete;
529
530         if (prefs_common.undolevels <= 0) return;
531         if (start_pos == end_pos) return;
532
533         text_to_delete = gtk_editable_get_chars(GTK_EDITABLE(editable),
534                                                 start_pos, end_pos);
535         undo_add(text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE,
536                  undostruct);
537         g_free(text_to_delete);
538 }
539
540 void undo_paste_clipboard_cb(GtkEditable *editable, UndoMain *undostruct)
541 {
542         if (editable->clipboard_text == NULL) return;
543
544         if (prefs_common.undolevels > 0)
545                 if (undo_get_selection(editable, NULL, NULL))
546                         undostruct->paste = TRUE;
547 }
548
549 /**
550  * undo_get_selection:
551  * @text: Text to get the selection from
552  * @start: return here the start position of the selection
553  * @end: return here the end position of the selection
554  *
555  * Gets the current selection for View
556  *
557  * Return Value: TRUE if there is a selection active, FALSE if not
558  **/
559 static gint undo_get_selection(GtkEditable *text, guint *start, guint *end) 
560 {
561         guint start_pos, end_pos;
562
563         start_pos = text->selection_start_pos;
564         end_pos   = text->selection_end_pos;
565
566         /* The user can select from end to start too. If so, swap it*/
567         if (end_pos < start_pos) {
568                 guint swap_pos;
569                 swap_pos  = end_pos;
570                 end_pos   = start_pos;
571                 start_pos = swap_pos;
572         }
573
574         if (start != NULL)
575                 *start = start_pos;
576                 
577         if (end != NULL)
578                 *end = end_pos;
579
580         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
581                 return TRUE;
582         else
583                 return FALSE;
584 }