fbcd5e2b61d3f8fcabc528aeff66b715d11e50ad
[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
36 typedef struct _UndoInfo UndoInfo;
37
38 struct _UndoInfo 
39 {
40         UndoAction action;
41         gchar *text;
42         gint start_pos;
43         gint end_pos;
44         gfloat window_position;
45         gint mergeable;
46 };
47
48 static void undo_free_list      (GList         **list_pointer);
49 static void undo_check_size     (UndoMain       *undostruct);
50 static gint undo_merge          (GList          *list,
51                                  guint           start_pos,
52                                  guint           end_pos,
53                                  gint            action,
54                                  const guchar   *text);
55 static void undo_add            (const gchar    *text,
56                                  gint            start_pos,
57                                  gint            end_pos,
58                                  UndoAction      action,
59                                  UndoMain       *undostruct);
60 static gint undo_get_selection  (GtkEditable    *text,
61                                  guint          *start,
62                                  guint          *end);
63 static void undo_insert_text_cb (GtkEditable    *editable,
64                                  gchar          *new_text,
65                                  gint            new_text_length,
66                                  gint           *position,
67                                  UndoMain       *undostruct);
68 static void undo_delete_text_cb (GtkEditable    *editable,
69                                  gint            start_pos,
70                                  gint            end_pos,
71                                  UndoMain       *undostruct);
72
73 static void undo_paste_clipboard_cb     (GtkEditable    *editable,
74                                          UndoMain       *undostruct);
75
76 void undo_undo                  (UndoMain       *undostruct);
77 void undo_redo                  (UndoMain       *undostruct);
78
79
80 UndoMain *undo_init(GtkWidget *text) 
81 {
82         UndoMain *undostruct;
83         
84         g_return_val_if_fail(text != NULL, NULL);
85
86         undostruct = g_new(UndoMain, 1);
87         undostruct->text = text;
88         undostruct->undo = NULL;
89         undostruct->redo = NULL;
90         undostruct->paste = 0;
91         undostruct->undo_state = FALSE;
92         undostruct->redo_state = FALSE;
93
94         gtk_signal_connect(GTK_OBJECT(text), "insert-text",
95                            GTK_SIGNAL_FUNC(undo_insert_text_cb), undostruct);
96         gtk_signal_connect(GTK_OBJECT(text), "delete-text",
97                            GTK_SIGNAL_FUNC(undo_delete_text_cb), undostruct);
98         gtk_signal_connect(GTK_OBJECT(text), "paste-clipboard",
99                            GTK_SIGNAL_FUNC(undo_paste_clipboard_cb), undostruct);
100
101         return undostruct;
102 }
103
104 void undo_destroy (UndoMain *undostruct) 
105 {
106         undo_free_list(&undostruct->undo);
107         undo_free_list(&undostruct->redo);
108         g_free(undostruct);
109 }
110
111 static UndoInfo *undo_object_new(gchar *text, gint start_pos, gint end_pos, 
112                                  UndoAction action, gfloat window_position) 
113 {
114         UndoInfo *undoinfo;
115         undoinfo = g_new (UndoInfo, 1);
116         undoinfo->text      = text;
117         undoinfo->start_pos = start_pos;
118         undoinfo->end_pos   = end_pos;
119         undoinfo->action    = action;
120         undoinfo->window_position = window_position;
121         return undoinfo;
122 }
123
124 static void undo_object_free(UndoInfo *undo) 
125 {
126         g_free (undo->text);
127         g_free (undo);
128 }
129
130 /**
131  * undo_free_list:
132  * @list_pointer: list to be freed
133  *
134  * frees and undo structure list
135  **/
136 static void undo_free_list(GList **list_pointer) 
137 {
138         UndoInfo *undo;
139         GList *cur, *list = *list_pointer;
140
141         if (list == NULL) return;
142
143         for (cur = list; cur != NULL; cur = cur->next) {
144                 undo = (UndoInfo *)cur->data;
145                 undo_object_free(undo);
146         }
147
148         g_list_free(list);
149         *list_pointer = NULL;
150 }
151
152 void undo_set_change_state_func(UndoMain *undostruct, UndoChangeStateFunc func,
153                                 gpointer data)
154 {
155         g_return_if_fail(undostruct != NULL);
156
157         undostruct->change_state_func = func;
158         undostruct->change_state_data = data;
159 }
160
161 /**
162  * undo_check_size:
163  * @compose: document to check
164  *
165  * Checks that the size of compose->undo does not excede settings->undo_levels and
166  * frees any undo level above sett->undo_level.
167  *
168  **/
169 static void undo_check_size(UndoMain *undostruct) 
170 {
171         UndoInfo *last_undo;
172         guint length;
173
174         if (prefs_common.undolevels < 1) return;
175
176         /* No need to check for the redo list size since the undo
177            list gets freed on any call to compose_undo_add */
178         length = g_list_length(undostruct->undo);
179         if (length >= prefs_common.undolevels && prefs_common.undolevels > 0) {
180                 last_undo = (UndoInfo *)g_list_last(undostruct->undo)->data;
181                 undostruct->undo = g_list_remove(undostruct->undo, last_undo);
182                 undo_object_free(last_undo);
183         }
184         debug_print("g_list_length(undostruct->undo): %d\n", length);
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         debug_print("Merged: %s\n", text);
283         return TRUE;
284 }
285
286 /**
287  * compose_undo_add:
288  * @text:
289  * @start_pos:
290  * @end_pos:
291  * @action: either UNDO_ACTION_INSERT or UNDO_ACTION_DELETE
292  * @compose:
293  * @view: The view so that we save the scroll bar position.
294  *
295  * Adds text to the undo stack. It also performs test to limit the number
296  * of undo levels and deltes the redo list
297  **/
298
299 static void undo_add(const gchar *text, 
300                      gint start_pos, gint end_pos,
301                      UndoAction action, UndoMain *undostruct) 
302 {
303         UndoInfo *undoinfo;
304
305         debug_print("undo_add(%i)*%s*\n", strlen (text), text);
306
307         g_return_if_fail(text != NULL);
308         g_return_if_fail(end_pos >= start_pos);
309
310         undo_free_list(&undostruct->redo);
311
312         /* Set the redo sensitivity */
313         undostruct->change_state_func(undostruct,
314                                       UNDO_STATE_UNCHANGED, UNDO_STATE_FALSE,
315                                       undostruct->change_state_data);
316
317         if (undostruct->paste != 0) {
318                 if (action == UNDO_ACTION_INSERT) 
319                         action = UNDO_ACTION_REPLACE_INSERT;
320                 else 
321                         action = UNDO_ACTION_REPLACE_DELETE;
322                 undostruct->paste = undostruct->paste + 1;
323                 if (undostruct->paste == 3) 
324                         undostruct->paste = 0;
325         }
326
327         if (undo_merge(undostruct->undo, start_pos, end_pos, action, text))
328                 return;
329
330         undo_check_size(undostruct);
331
332         debug_print("New: %s Action: %d Paste: %d\n", text, action, undostruct->paste);
333
334         undoinfo = undo_object_new(g_strdup(text), start_pos, end_pos, action,
335                                    GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj)->value);
336
337         if (end_pos - start_pos != 1 || text[0] == '\n')
338                 undoinfo->mergeable = FALSE;
339         else
340                 undoinfo->mergeable = TRUE;
341
342         undostruct->undo = g_list_prepend(undostruct->undo, undoinfo);
343
344         undostruct->change_state_func(undostruct,
345                                       UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED,
346                                       undostruct->change_state_data);
347 }
348
349 /**
350  * undo_undo:
351  * @w: not used
352  * @data: not used
353  *
354  * Executes an undo request on the current document
355  **/
356 void undo_undo(UndoMain *undostruct) 
357 {
358         UndoInfo *undoinfo;
359         guint start_pos, end_pos;
360
361         g_return_if_fail(undostruct != NULL);
362
363         if (undostruct->undo == NULL) return;
364
365         /* The undo data we need is always at the top op the
366            stack. So, therefore, the first one */
367         undoinfo = (UndoInfo *)undostruct->undo->data;
368         g_return_if_fail(undoinfo != NULL);
369         undoinfo->mergeable = FALSE;
370         undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
371         undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
372
373         /* Check if there is a selection active */
374         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
375         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
376         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
377                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text),
378                                            0, 0);
379
380         /* Move the view (scrollbars) to the correct position */
381         gtk_adjustment_set_value
382                 (GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj),
383                  undoinfo->window_position);
384
385         switch (undoinfo->action) {
386         case UNDO_ACTION_DELETE:
387                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
388                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
389                 debug_print("UNDO_ACTION_DELETE %s\n", undoinfo->text);
390                 break;
391         case UNDO_ACTION_INSERT:
392                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
393                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
394                 debug_print("UNDO_ACTION_INSERT %d\n", undoinfo->end_pos-undoinfo->start_pos);
395                 break;
396         case UNDO_ACTION_REPLACE_INSERT:
397                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
398                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
399                 debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
400                 /* "pull" another data structure from the list */
401                 undoinfo = (UndoInfo *)undostruct->undo->data;
402                 g_return_if_fail(undoinfo != NULL);
403                 undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
404                 undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
405                 g_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
406                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
407                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
408                 debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
409                 break;
410         case UNDO_ACTION_REPLACE_DELETE:
411                 g_warning("This should not happen. UNDO_REPLACE_DELETE");
412                 break;
413         default:
414                 g_assert_not_reached();
415                 break;
416         }
417
418         undostruct->change_state_func(undostruct,
419                                       UNDO_STATE_UNCHANGED, UNDO_STATE_TRUE,
420                                       undostruct->change_state_data);
421
422         if (undostruct->undo == NULL)
423                 undostruct->change_state_func(undostruct,
424                                               UNDO_STATE_FALSE,
425                                               UNDO_STATE_UNCHANGED,
426                                               undostruct->change_state_data);
427 }
428
429 /**
430  * undo_redo:
431  * @w: not used
432  * @data: not used
433  *
434  * executes a redo request on the current document
435  **/
436 void undo_redo(UndoMain *undostruct) 
437 {
438         UndoInfo *redoinfo;
439         guint start_pos, end_pos;
440
441         g_return_if_fail(undostruct != NULL);
442
443         if (undostruct->redo == NULL) return;
444
445         redoinfo = (UndoInfo *)undostruct->redo->data;
446         g_return_if_fail (redoinfo != NULL);
447         undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
448         undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
449
450         /* Check if there is a selection active */
451         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
452         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
453         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
454                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text), 0, 0);
455
456         /* Move the view to the right position. */
457         gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj), 
458                                  redoinfo->window_position);
459
460         switch (redoinfo->action) {
461         case UNDO_ACTION_INSERT:
462                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
463                                    redoinfo->start_pos);
464                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
465                                 NULL, redoinfo->text, -1);
466                 debug_print("UNDO_ACTION_DELETE %s\n",redoinfo->text);
467                 break;
468         case UNDO_ACTION_DELETE:
469                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
470                                    redoinfo->end_pos);
471                 gtk_stext_backward_delete
472                         (GTK_STEXT(undostruct->text), 
473                          redoinfo->end_pos - redoinfo->start_pos);
474                 debug_print("UNDO_ACTION_INSERT %d\n", 
475                             redoinfo->end_pos-redoinfo->start_pos);
476                 break;
477         case UNDO_ACTION_REPLACE_DELETE:
478                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
479                                    redoinfo->end_pos);
480                 gtk_stext_backward_delete
481                         (GTK_STEXT(undostruct->text), 
482                          redoinfo->end_pos - redoinfo->start_pos);
483                 /* "pull" another data structure from the list */
484                 redoinfo = (UndoInfo *)undostruct->redo->data;
485                 g_return_if_fail(redoinfo != NULL);
486                 undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
487                 undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
488                 g_return_if_fail(redoinfo->action == UNDO_ACTION_REPLACE_INSERT);
489                 gtk_stext_set_point(GTK_STEXT(undostruct->text),
490                                    redoinfo->start_pos);
491                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
492                                 NULL, redoinfo->text, -1);
493                 break;
494         case UNDO_ACTION_REPLACE_INSERT:
495                 g_warning("This should not happen. Redo: UNDO_REPLACE_INSERT");
496                 break;
497         default:
498                 g_assert_not_reached();
499                 break;
500         }
501
502         undostruct->change_state_func(undostruct,
503                                       UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED, 
504                                       undostruct->change_state_data);
505
506         if (undostruct->redo == NULL)
507                 undostruct->change_state_func(undostruct,
508                                               UNDO_STATE_UNCHANGED,
509                                               UNDO_STATE_FALSE,
510                                               undostruct->change_state_data);
511 }
512
513 void undo_insert_text_cb(GtkEditable *editable, gchar *new_text,
514                          gint new_text_length, gint *position, 
515                          UndoMain *undostruct) 
516 {
517         gchar *text_to_insert;
518         size_t wlen;
519
520         if (prefs_common.undolevels <= 0) return;
521
522         Xstrndup_a(text_to_insert, new_text, new_text_length, return);
523         if (MB_CUR_MAX > 1) {
524                 wchar_t *wstr;
525
526                 wstr = g_new(wchar_t, new_text_length + 1);
527                 wlen = mbstowcs(wstr, text_to_insert, new_text_length + 1);
528                 g_free(wstr);
529                 if (wlen < 0) return;
530         } else
531                 wlen = new_text_length;
532
533         undo_add(text_to_insert, *position, *position + wlen,
534                  UNDO_ACTION_INSERT, undostruct);
535 }
536
537 void undo_delete_text_cb(GtkEditable *editable, gint start_pos,
538                          gint end_pos, UndoMain *undostruct) 
539 {
540         gchar *text_to_delete;
541
542         if (prefs_common.undolevels <= 0) return;
543         if (start_pos == end_pos) return;
544
545         text_to_delete = gtk_editable_get_chars(GTK_EDITABLE(editable),
546                                                 start_pos, end_pos);
547         undo_add(text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE,
548                  undostruct);
549         g_free(text_to_delete);
550 }
551
552 void undo_paste_clipboard_cb(GtkEditable *editable, UndoMain *undostruct)
553 {
554         if (editable->clipboard_text == NULL) return;
555
556         debug_print("before 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 }