fix for multibyte characters in undo.c.
[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 "gtkstext.h"
35 #include "prefs_common.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 *nth_redo;
140         GList *cur, *list = *list_pointer;
141
142         if (list == NULL)
143                 return;
144
145         debug_print("length of list: %d\n", g_list_length(list));
146
147         for (cur = list; cur != NULL; cur = cur->next) {
148                 nth_redo = cur->data;
149                 undo_object_free(nth_redo);
150         }
151
152         g_list_free(list);
153         *list_pointer = NULL;
154 }
155
156 void undo_set_undo_change_funct(UndoMain *undostruct, UndoChangeState func, 
157                                 GtkWidget *changewidget) 
158 {
159         g_return_if_fail(undostruct != NULL);
160         undostruct->change_func = func;
161         undostruct->changewidget = changewidget;
162 }
163
164 /**
165  * undo_check_size:
166  * @compose: document to check
167  *
168  * Checks that the size of compose->undo does not excede settings->undo_levels and
169  * frees any undo level above sett->undo_level.
170  *
171  **/
172 static void undo_check_size(UndoMain *undostruct) 
173 {
174         UndoInfo *nth_undo;
175
176         if (prefs_common.undolevels < 1)
177                 return;
178
179         /* No need to check for the redo list size since the undo
180            list gets freed on any call to compose_undo_add */
181         if (g_list_length(undostruct->undo) >= prefs_common.undolevels && prefs_common.undolevels > 0) {
182                 nth_undo = g_list_nth_data(undostruct->undo, g_list_length(undostruct->undo) - 1);
183                 undostruct->undo = g_list_remove(undostruct->undo, nth_undo);
184                 g_free (nth_undo->text);
185                 g_free (nth_undo);
186         }
187         debug_print("g_list_length (undostruct->undo): %d\n", g_list_length(undostruct->undo));
188 }
189
190 /**
191  * undo_merge:
192  * @last_undo:
193  * @start_pos:
194  * @end_pos:
195  * @action:
196  *
197  * This function tries to merge the undo object at the top of
198  * the stack with a new set of data. So when we undo for example
199  * typing, we can undo the whole word and not each letter by itself
200  *
201  * Return Value: TRUE is merge was sucessful, FALSE otherwise
202  **/
203 static gint undo_merge (GList *list, guint start_pos, guint end_pos, gint action, const guchar* text) 
204 {
205         guchar *temp_string;
206         UndoInfo *last_undo;
207         gboolean checkit = TRUE;
208
209         /* This are the cases in which we will NOT merge :
210            1. if (last_undo->mergeable == FALSE)
211            [mergeable = FALSE when the size of the undo data was not 1.
212            or if the data was size = 1 but = '\n' or if the undo object
213            has been "undone" already ]
214            2. The size of text is not 1
215            3. If the new merging data is a '\n'
216            4. If the last char of the undo_last data is a space/tab
217            and the new char is not a space/tab ( so that we undo
218            words and not chars )
219            5. If the type (action) of undo is different from the last one
220         Chema */
221
222         if (list == NULL)
223                 return FALSE;
224
225         last_undo = list->data;
226
227         if (!last_undo->mergeable)
228                 return FALSE;
229
230         if (end_pos-start_pos != 1) {
231                 last_undo->mergeable = FALSE;
232                 return FALSE;
233         }
234
235         if (text[0] == '\n') 
236                 checkit = FALSE;
237
238         if (action != last_undo->action) 
239                 checkit = FALSE;
240
241         if (action == UNDO_ACTION_REPLACE_INSERT || action == UNDO_ACTION_REPLACE_DELETE)
242                  checkit = FALSE;
243
244         if (action == UNDO_ACTION_DELETE && checkit) {
245                 if (last_undo->start_pos!=end_pos && last_undo->start_pos != start_pos && checkit)
246                          checkit = FALSE;
247
248                 if (last_undo->start_pos == start_pos && checkit) {
249                         /* Deleted with the delete key */
250                         if ( text[0] != ' ' && text[0] != '\t' && checkit &&
251                              (last_undo->text[last_undo->end_pos-last_undo->start_pos - 1] == ' '
252                               || last_undo->text[last_undo->end_pos-last_undo->start_pos - 1] == '\t'))
253                                  checkit = FALSE;
254
255                         temp_string = g_strdup_printf("%s%s", last_undo->text, text);
256                         g_free(last_undo->text);
257                         last_undo->end_pos += 1;
258                         last_undo->text = temp_string;
259                 }
260                 else if (checkit) {
261                         /* Deleted with the backspace key */
262                         if ( text[0] != ' ' && text[0] != '\t' && checkit &&
263                              (last_undo->text[0] == ' '
264                               || last_undo->text[0] == '\t'))
265                                  checkit = FALSE;
266
267                         temp_string = g_strdup_printf("%s%s", text, last_undo->text);
268                         g_free(last_undo->text);
269                         last_undo->start_pos = start_pos;
270                         last_undo->text = temp_string;
271                 }
272         }
273         else if (action == UNDO_ACTION_INSERT && checkit) {
274                 if (last_undo->end_pos != start_pos && checkit)
275                          checkit = FALSE;
276
277 /*                if ( text[0]!=' ' && text[0]!='\t' &&
278                      (last_undo->text [last_undo->end_pos-last_undo->start_pos - 1] ==' '
279                       || last_undo->text [last_undo->end_pos-last_undo->start_pos - 1] == '\t'))
280                         goto compose_undo_do_not_merge;
281 */
282                 if (checkit) {
283                         temp_string = g_strdup_printf("%s%s", last_undo->text, text);
284                         g_free(last_undo->text);
285                         last_undo->end_pos = end_pos;
286                         last_undo->text = temp_string;
287                 }
288         }
289         else if (checkit)
290                 debug_print("Unknown action [%i] inside undo merge encountered", action);
291
292         if (checkit) {
293                 debug_print("Merged: %s\n", text);
294                 return TRUE;
295         }
296         else {
297                 last_undo->mergeable = FALSE;
298                 return FALSE;
299         }
300 }
301
302 /**
303  * compose_undo_add:
304  * @text:
305  * @start_pos:
306  * @end_pos:
307  * @action: either UNDO_ACTION_INSERT or UNDO_ACTION_DELETE
308  * @compose:
309  * @view: The view so that we save the scroll bar position.
310  *
311  * Adds text to the undo stack. It also performs test to limit the number
312  * of undo levels and deltes the redo list
313  **/
314
315 static void undo_add(const gchar *text, 
316                      gint start_pos, gint end_pos,
317                      UndoAction action, UndoMain *undostruct) 
318 {
319         UndoInfo *undoinfo;
320
321         debug_print("undo_add(%i)*%s*\n", strlen (text), text);
322
323         g_return_if_fail(text != NULL);
324         g_return_if_fail(end_pos >= start_pos);
325
326         undo_free_list(&undostruct->redo);
327
328         /* Set the redo sensitivity */
329         undostruct->change_func(undostruct, UNDO_STATE_UNCHANGED, UNDO_STATE_FALSE, 
330                                 undostruct->changewidget);
331
332         if (undostruct->paste != 0) {
333                 if (action == UNDO_ACTION_INSERT) 
334                         action = UNDO_ACTION_REPLACE_INSERT;
335                 else 
336                         action = UNDO_ACTION_REPLACE_DELETE;
337                 undostruct->paste = undostruct->paste + 1;
338                 if (undostruct->paste == 3) 
339                         undostruct->paste = 0;
340         }
341
342         if (undo_merge(undostruct->undo, start_pos, end_pos, action, text))
343                 return;
344
345         undo_check_size(undostruct);
346
347         debug_print("New: %s Action: %d Paste: %d\n", text, action, undostruct->paste);
348
349         undoinfo = undo_object_new(g_strdup(text), start_pos, end_pos, action,
350                                    GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj)->value);
351
352         if (end_pos-start_pos != 1 || text[0] == '\n')
353                 undoinfo->mergeable = FALSE;
354         else
355                 undoinfo->mergeable = TRUE;
356
357         undostruct->undo = g_list_prepend(undostruct->undo, undoinfo);
358
359         undostruct->change_func(undostruct, UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED, undostruct->changewidget);
360 }
361
362 /**
363  * undo_undo:
364  * @w: not used
365  * @data: not used
366  *
367  * Executes an undo request on the current document
368  **/
369 void undo_undo(UndoMain *undostruct) 
370 {
371         UndoInfo *undoinfo;
372         guint start_pos, end_pos;
373
374         if (undostruct->undo == NULL)
375                 return;
376
377         g_return_if_fail(undostruct != NULL);
378
379
380         /* The undo data we need is always at the top op the
381            stack. So, therefore, the first one */
382         undoinfo = g_list_nth_data(undostruct->undo, 0);
383         g_return_if_fail(undoinfo != NULL);
384         undoinfo->mergeable = FALSE;
385         undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
386         undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
387
388         /* Check if there is a selection active */
389         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
390         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
391         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
392                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text), 0, 0);
393
394         /* Move the view (scrollbars) to the correct position */
395         gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj), undoinfo->window_position);
396
397         switch (undoinfo->action) {
398         case UNDO_ACTION_DELETE:
399                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
400                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
401                 debug_print("UNDO_ACTION_DELETE %s\n", undoinfo->text);
402                 break;
403         case UNDO_ACTION_INSERT:
404                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
405                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
406                 debug_print("UNDO_ACTION_INSERT %d\n", undoinfo->end_pos-undoinfo->start_pos);
407                 break;
408         case UNDO_ACTION_REPLACE_INSERT:
409                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
410                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
411                 debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
412                 /* "pull" another data structure from the list */
413                 undoinfo = g_list_nth_data(undostruct->undo, 0);
414                 g_return_if_fail(undoinfo != NULL);
415                 undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
416                 undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
417                 g_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
418                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
419                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
420                 debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
421                 break;
422         case UNDO_ACTION_REPLACE_DELETE:
423                 g_warning("This should not happen. UNDO_REPLACE_DELETE");
424                 break;
425         default:
426                 g_assert_not_reached();
427                 break;
428         }
429
430         undostruct->change_func(undostruct, UNDO_STATE_UNCHANGED, 
431                                 UNDO_STATE_TRUE, undostruct->changewidget);
432                                 
433         if (g_list_length (undostruct->undo) == 0)
434                 undostruct->change_func(undostruct, UNDO_STATE_FALSE, 
435                                         UNDO_STATE_UNCHANGED, 
436                                         undostruct->changewidget);
437 }
438
439 /**
440  * undo_redo:
441  * @w: not used
442  * @data: not used
443  *
444  * executes a redo request on the current document
445  **/
446 void undo_redo(UndoMain *undostruct) 
447 {
448         UndoInfo *redoinfo;
449         guint start_pos, end_pos;
450
451         if (undostruct->redo == NULL)
452                 return;
453
454         if (undostruct==NULL)
455                 return;
456
457         redoinfo = g_list_nth_data(undostruct->redo, 0);
458         g_return_if_fail (redoinfo!=NULL);
459         undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
460         undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
461
462         /* Check if there is a selection active */
463         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
464         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
465         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
466                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text), 0, 0);
467
468         /* Move the view to the right position. */
469         gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj), 
470                                  redoinfo->window_position);
471
472         switch (redoinfo->action) {
473         case UNDO_ACTION_INSERT:
474                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->start_pos);
475                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
476                                  NULL, redoinfo->text, -1);
477                 debug_print("UNDO_ACTION_DELETE %s\n",redoinfo->text);
478                 break;
479         case UNDO_ACTION_DELETE:
480                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->end_pos);
481                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), 
482                                           redoinfo->end_pos-redoinfo->start_pos);
483                 debug_print("UNDO_ACTION_INSERT %d\n", 
484                             redoinfo->end_pos-redoinfo->start_pos);
485                 break;
486         case UNDO_ACTION_REPLACE_DELETE:
487                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->end_pos);
488                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), 
489                                           redoinfo->end_pos-redoinfo->start_pos);
490                 /* "pull" another data structure from the list */
491                 redoinfo = g_list_nth_data(undostruct->redo, 0);
492                 g_return_if_fail(redoinfo != NULL);
493                 undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
494                 undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
495                 g_return_if_fail(redoinfo->action==UNDO_ACTION_REPLACE_INSERT);
496                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->start_pos);
497                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
498                                  NULL, redoinfo->text, -1);
499                 break;
500         case UNDO_ACTION_REPLACE_INSERT:
501                 g_warning("This should not happen. Redo: UNDO_REPLACE_INSERT");
502                 break;
503         default:
504                 g_assert_not_reached();
505                 break;
506         }
507
508         undostruct->change_func(undostruct, UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED, 
509                                 undostruct->changewidget);
510                                 
511         if (g_list_length(undostruct->redo) == 0)
512                 undostruct->change_func(undostruct, UNDO_STATE_UNCHANGED, 
513                                         UNDO_STATE_FALSE, undostruct->changewidget);
514 }
515
516 void undo_insert_text_cb(GtkEditable *editable, gchar *new_text,
517                          gint new_text_length, gint *position, 
518                          UndoMain *undostruct) 
519 {
520         guchar *text_to_insert;
521         size_t wlen;
522
523         if (prefs_common.undolevels <= 0) return;
524
525         Xstrndup_a(text_to_insert, new_text, new_text_length, return);
526         if (MB_CUR_MAX > 1) {
527                 wchar_t *wstr;
528
529                 Xalloca(wstr, sizeof(wchar_t) * (new_text_length + 1), return);
530                 wlen = mbstowcs(wstr, text_to_insert, new_text_length + 1);
531                 if (wlen < 0) return;
532         } else
533                 wlen = new_text_length;
534
535         undo_add(text_to_insert, *position, *position + wlen,
536                  UNDO_ACTION_INSERT, undostruct);
537 }
538
539 void undo_delete_text_cb(GtkEditable *editable, gint start_pos,
540                          gint end_pos, UndoMain *undostruct) 
541 {
542         guchar *text_to_delete;
543
544         if (prefs_common.undolevels <= 0) return;
545         if (start_pos == end_pos) return;
546
547         text_to_delete = gtk_editable_get_chars(GTK_EDITABLE(editable),
548                                                 start_pos, end_pos);
549         undo_add(text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE,
550                  undostruct);
551         g_free (text_to_delete);
552 }
553
554 void undo_paste_clipboard_cb (GtkEditable *editable, UndoMain *undostruct) 
555 {
556         debug_print("befor Paste: %d\n", undostruct->paste);
557         if (prefs_common.undolevels > 0)
558                 if (undo_get_selection(editable, NULL, NULL))
559                         undostruct->paste = TRUE;
560         debug_print("after Paste: %d\n", undostruct->paste);
561 }
562
563 /**
564  * undo_get_selection:
565  * @text: Text to get the selection from
566  * @start: return here the start position of the selection
567  * @end: return here the end position of the selection
568  *
569  * Gets the current selection for View
570  *
571  * Return Value: TRUE if there is a selection active, FALSE if not
572  **/
573 static gint undo_get_selection(GtkEditable *text, guint *start, guint *end) 
574 {
575         guint start_pos, end_pos;
576
577         start_pos = text->selection_start_pos;
578         end_pos   = text->selection_end_pos;
579
580         /* The user can select from end to start too. If so, swap it*/
581         if (end_pos < start_pos) {
582                 guint swap_pos;
583                 swap_pos  = end_pos;
584                 end_pos   = start_pos;
585                 start_pos = swap_pos;
586         }
587
588         if (start != NULL)
589                 *start = start_pos;
590                 
591         if (end != NULL)
592                 *end = end_pos;
593
594         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
595                 return TRUE;
596         else
597                 return FALSE;
598 }