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