changes to the reply, forward, email/news buttons (popup menu now invoked with right...
[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 #include <config.h>
24 #include <string.h> /* For strlen */
25
26 #include "undo.h"
27 #include "utils.h"
28 #include "gtkstext.h"
29
30 typedef struct _UndoInfo UndoInfo;
31
32 struct _UndoInfo 
33 {
34         UndoAction action;
35         gchar *text;
36         gint start_pos;
37         gint end_pos;
38         gfloat window_position;
39         gint mergeable;
40 };
41
42 static void undo_free_list      (GList **list_pointer);
43 static void undo_check_size     (UndoMain *undostruct);
44 static gint undo_merge          (GList *list, guint start_pos,
45                                  guint end_pos, gint action, 
46                                  const guchar *text);
47 static void undo_add            (const gchar *text, gint start_pos, gint end_pos,
48                                  UndoAction action, UndoMain *undostruct);
49 static gint undo_get_selection  (GtkEditable *text, guint *start, guint *end);
50 static void undo_insert_text_cb (GtkEditable *editable, gchar *new_text,
51                                  gint new_text_length, gint *position, 
52                                  UndoMain *undostruct);
53 static void undo_delete_text_cb (GtkEditable *editable, gint start_pos,
54                                  gint end_pos, UndoMain *undostruct);
55 static void undo_paste_clipboard_cb (GtkEditable *editable, UndoMain *undostruct);
56 void undo_undo                  (UndoMain *undostruct);
57 void undo_redo                  (UndoMain *undostruct);
58
59
60 UndoMain *undo_init (GtkWidget *text) 
61 {
62         UndoMain *undostruct;
63         
64         g_return_if_fail(text != NULL);
65
66         undostruct = g_new(UndoMain, 1);
67         undostruct->text = text;
68         undostruct->undo = NULL;
69         undostruct->redo = NULL;
70         undostruct->paste = 0;
71         undostruct->undo_state = FALSE;
72         undostruct->redo_state = FALSE;
73         debug_print("undostruct: %x\n", &undostruct);
74
75         gtk_signal_connect(GTK_OBJECT(text), "insert-text",
76                            GTK_SIGNAL_FUNC(undo_insert_text_cb), undostruct);
77         gtk_signal_connect(GTK_OBJECT(text), "delete-text",
78                            GTK_SIGNAL_FUNC(undo_delete_text_cb), undostruct);
79         gtk_signal_connect(GTK_OBJECT(text), "paste-clipboard",
80                            GTK_SIGNAL_FUNC(undo_paste_clipboard_cb), undostruct);
81
82         return undostruct;
83 }
84
85 void undo_destroy (UndoMain *undostruct) 
86 {
87         undo_free_list(&undostruct->undo);
88         undo_free_list(&undostruct->redo);
89         g_free(undostruct);
90 }
91
92 static UndoInfo *undo_object_new(gchar *text, gint start_pos, gint end_pos, 
93                                  UndoAction action, gfloat window_position) 
94 {
95         UndoInfo *undoinfo;
96         undoinfo = g_new (UndoInfo, 1);
97         undoinfo->text      = text;
98         undoinfo->start_pos = start_pos;
99         undoinfo->end_pos   = end_pos;
100         undoinfo->action    = action;
101         undoinfo->window_position = window_position;
102         return undoinfo;
103 }
104
105 static void undo_object_free(UndoInfo *undo) 
106 {
107         g_free (undo->text);
108         g_free (undo);
109 }
110
111 /**
112  * undo_free_list:
113  * @list_pointer: list to be freed
114  *
115  * frees and undo structure list
116  **/
117 static void undo_free_list(GList **list_pointer) 
118 {
119         gint n;
120         UndoInfo *nth_redo;
121         GList *cur, *list = *list_pointer;
122
123         if (list == NULL)
124                 return;
125
126         debug_print("length of list: %d\n", g_list_length(list));
127
128         for (cur = list; cur != NULL; cur = cur->next) {
129                 nth_redo = cur->data;
130                 undo_object_free(nth_redo);
131         }
132
133         g_list_free(list);
134         *list_pointer = NULL;
135 }
136
137 void undo_set_undo_change_funct(UndoMain *undostruct, UndoChangeState func, 
138                                 GtkWidget *changewidget) 
139 {
140         g_return_if_fail(undostruct != NULL);
141         undostruct->change_func = func;
142         undostruct->changewidget = changewidget;
143 }
144
145 /**
146  * undo_check_size:
147  * @compose: document to check
148  *
149  * Checks that the size of compose->undo does not excede settings->undo_levels and
150  * frees any undo level above sett->undo_level.
151  *
152  **/
153 static void undo_check_size(UndoMain *undostruct) 
154 {
155         gint n;
156         UndoInfo *nth_undo;
157         gint undo_levels = 10;
158
159         if (undo_levels < 1)
160                 return;
161
162         /* No need to check for the redo list size since the undo
163            list gets freed on any call to compose_undo_add */
164         if (g_list_length(undostruct->undo) >= undo_levels && undo_levels > 0) {
165                 nth_undo = g_list_nth_data(undostruct->undo, g_list_length(undostruct->undo) - 1);
166                 undostruct->undo = g_list_remove(undostruct->undo, nth_undo);
167                 g_free (nth_undo->text);
168                 g_free (nth_undo);
169         }
170         debug_print("g_list_length (undostruct->undo): %d\n", g_list_length(undostruct->undo));
171 }
172
173 /**
174  * undo_merge:
175  * @last_undo:
176  * @start_pos:
177  * @end_pos:
178  * @action:
179  *
180  * This function tries to merge the undo object at the top of
181  * the stack with a new set of data. So when we undo for example
182  * typing, we can undo the whole word and not each letter by itself
183  *
184  * Return Value: TRUE is merge was sucessful, FALSE otherwise
185  **/
186 static gint undo_merge (GList *list, guint start_pos, guint end_pos, gint action, const guchar* text) 
187 {
188         guchar *temp_string;
189         UndoInfo *last_undo;
190         gboolean checkit = TRUE;
191
192         /* This are the cases in which we will NOT merge :
193            1. if (last_undo->mergeable == FALSE)
194            [mergeable = FALSE when the size of the undo data was not 1.
195            or if the data was size = 1 but = '\n' or if the undo object
196            has been "undone" already ]
197            2. The size of text is not 1
198            3. If the new merging data is a '\n'
199            4. If the last char of the undo_last data is a space/tab
200            and the new char is not a space/tab ( so that we undo
201            words and not chars )
202            5. If the type (action) of undo is different from the last one
203         Chema */
204
205         if (list == NULL)
206                 return FALSE;
207
208         last_undo = list->data;
209
210         if (!last_undo->mergeable)
211                 return FALSE;
212
213         if (end_pos-start_pos != 1) {
214                 last_undo->mergeable = FALSE;
215                 return FALSE;
216         }
217
218         if (text[0] == '\n') 
219                 checkit = FALSE;
220
221         if (action != last_undo->action) 
222                 checkit = FALSE;
223
224         if (action == UNDO_ACTION_REPLACE_INSERT || action == UNDO_ACTION_REPLACE_DELETE)
225                  checkit = FALSE;
226
227         if (action == UNDO_ACTION_DELETE && checkit) {
228                 if (last_undo->start_pos!=end_pos && last_undo->start_pos != start_pos && checkit)
229                          checkit = FALSE;
230
231                 if (last_undo->start_pos == start_pos && checkit) {
232                         /* Deleted with the delete key */
233                         if ( text[0] != ' ' && text[0] != '\t' && checkit &&
234                              (last_undo->text[last_undo->end_pos-last_undo->start_pos - 1] == ' '
235                               || last_undo->text[last_undo->end_pos-last_undo->start_pos - 1] == '\t'))
236                                  checkit = FALSE;
237
238                         temp_string = g_strdup_printf("%s%s", last_undo->text, text);
239                         g_free(last_undo->text);
240                         last_undo->end_pos += 1;
241                         last_undo->text = temp_string;
242                 }
243                 else if (checkit) {
244                         /* Deleted with the backspace key */
245                         if ( text[0] != ' ' && text[0] != '\t' && checkit &&
246                              (last_undo->text[0] == ' '
247                               || last_undo->text[0] == '\t'))
248                                  checkit = FALSE;
249
250                         temp_string = g_strdup_printf("%s%s", text, last_undo->text);
251                         g_free(last_undo->text);
252                         last_undo->start_pos = start_pos;
253                         last_undo->text = temp_string;
254                 }
255         }
256         else if (action == UNDO_ACTION_INSERT && checkit) {
257                 if (last_undo->end_pos != start_pos && checkit)
258                          checkit = FALSE;
259
260 /*                if ( text[0]!=' ' && text[0]!='\t' &&
261                      (last_undo->text [last_undo->end_pos-last_undo->start_pos - 1] ==' '
262                       || last_undo->text [last_undo->end_pos-last_undo->start_pos - 1] == '\t'))
263                         goto compose_undo_do_not_merge;
264 */
265                 if (checkit) {
266                         temp_string = g_strdup_printf("%s%s", last_undo->text, text);
267                         g_free(last_undo->text);
268                         last_undo->end_pos = end_pos;
269                         last_undo->text = temp_string;
270                 }
271         }
272         else if (checkit)
273                 debug_print("Unknown action [%i] inside undo merge encountered", action);
274
275         if (checkit) {
276                 debug_print("Merged: %s\n", text);
277                 return TRUE;
278         }
279         else {
280                 last_undo->mergeable = FALSE;
281                 return FALSE;
282         }
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         debug_print("undo_add(%i)*%s*\n", strlen (text), text);
305
306         g_return_if_fail(text != NULL);
307         g_return_if_fail(end_pos >= start_pos);
308
309         undo_free_list(&undostruct->redo);
310
311         /* Set the redo sensitivity */
312         undostruct->change_func(undostruct, UNDO_STATE_UNCHANGED, UNDO_STATE_FALSE, 
313                                 undostruct->changewidget);
314
315         if (undostruct->paste != 0) {
316                 if (action == UNDO_ACTION_INSERT) 
317                         action = UNDO_ACTION_REPLACE_INSERT;
318                 else 
319                         action = UNDO_ACTION_REPLACE_DELETE;
320                 undostruct->paste = undostruct->paste + 1;
321                 if (undostruct->paste == 3) 
322                         undostruct->paste = 0;
323         }
324
325         if (undo_merge(undostruct->undo, start_pos, end_pos, action, text))
326                 return;
327
328         undo_check_size(undostruct);
329
330         debug_print("New: %s Action: %d Paste: %d\n", text, action, undostruct->paste);
331
332         undoinfo = undo_object_new(g_strdup(text), start_pos, end_pos, action,
333                                    GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj)->value);
334
335         if (end_pos-start_pos != 1 || text[0] == '\n')
336                 undoinfo->mergeable = FALSE;
337         else
338                 undoinfo->mergeable = TRUE;
339
340         undostruct->undo = g_list_prepend(undostruct->undo, undoinfo);
341
342         undostruct->change_func(undostruct, UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED, undostruct->changewidget);
343 }
344
345 /**
346  * undo_undo:
347  * @w: not used
348  * @data: not used
349  *
350  * Executes an undo request on the current document
351  **/
352 void undo_undo(UndoMain *undostruct) 
353 {
354         UndoInfo *undoinfo;
355         guint start_pos, end_pos;
356
357         if (undostruct->undo == NULL)
358                 return;
359
360         g_return_if_fail(undostruct != NULL);
361
362
363         /* The undo data we need is always at the top op the
364            stack. So, therefore, the first one */
365         undoinfo = g_list_nth_data(undostruct->undo, 0);
366         g_return_if_fail(undoinfo != NULL);
367         undoinfo->mergeable = FALSE;
368         undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
369         undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
370
371         /* Check if there is a selection active */
372         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
373         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
374         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
375                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text), 0, 0);
376
377         /* Move the view (scrollbars) to the correct position */
378         gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj), 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                 debug_print("UNDO_ACTION_DELETE %s\n", undoinfo->text);
385                 break;
386         case UNDO_ACTION_INSERT:
387                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->end_pos);
388                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), undoinfo->end_pos-undoinfo->start_pos);
389                 debug_print("UNDO_ACTION_INSERT %d\n", undoinfo->end_pos-undoinfo->start_pos);
390                 break;
391         case UNDO_ACTION_REPLACE_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_REPLACE %s\n", undoinfo->text);
395                 /* "pull" another data structure from the list */
396                 undoinfo = g_list_nth_data(undostruct->undo, 0);
397                 g_return_if_fail(undoinfo != NULL);
398                 undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
399                 undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
400                 g_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
401                 gtk_stext_set_point(GTK_STEXT(undostruct->text), undoinfo->start_pos);
402                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, NULL, undoinfo->text, -1);
403                 debug_print("UNDO_ACTION_REPLACE %s\n", undoinfo->text);
404                 break;
405         case UNDO_ACTION_REPLACE_DELETE:
406                 g_warning("This should not happen. UNDO_REPLACE_DELETE");
407                 break;
408         default:
409                 g_assert_not_reached();
410                 break;
411         }
412
413         undostruct->change_func(undostruct, UNDO_STATE_UNCHANGED, 
414                                 UNDO_STATE_TRUE, undostruct->changewidget);
415                                 
416         if (g_list_length (undostruct->undo) == 0)
417                 undostruct->change_func(undostruct, UNDO_STATE_FALSE, 
418                                         UNDO_STATE_UNCHANGED, 
419                                         undostruct->changewidget);
420 }
421
422 /**
423  * undo_redo:
424  * @w: not used
425  * @data: not used
426  *
427  * executes a redo request on the current document
428  **/
429 void undo_redo(UndoMain *undostruct) 
430 {
431         UndoInfo *redoinfo;
432         guint start_pos, end_pos;
433
434         if (undostruct->redo == NULL)
435                 return;
436
437         if (undostruct==NULL)
438                 return;
439
440         redoinfo = g_list_nth_data(undostruct->redo, 0);
441         g_return_if_fail (redoinfo!=NULL);
442         undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
443         undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
444
445         /* Check if there is a selection active */
446         start_pos = GTK_EDITABLE(undostruct->text)->selection_start_pos;
447         end_pos   = GTK_EDITABLE(undostruct->text)->selection_end_pos;
448         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
449                 gtk_editable_select_region(GTK_EDITABLE(undostruct->text), 0, 0);
450
451         /* Move the view to the right position. */
452         gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_STEXT(undostruct->text)->vadj), 
453                                  redoinfo->window_position);
454
455         switch (redoinfo->action) {
456         case UNDO_ACTION_INSERT:
457                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->start_pos);
458                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
459                                  NULL, redoinfo->text, -1);
460                 debug_print("UNDO_ACTION_DELETE %s\n",redoinfo->text);
461                 break;
462         case UNDO_ACTION_DELETE:
463                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->end_pos);
464                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), 
465                                           redoinfo->end_pos-redoinfo->start_pos);
466                 debug_print("UNDO_ACTION_INSERT %d\n", 
467                             redoinfo->end_pos-redoinfo->start_pos);
468                 break;
469         case UNDO_ACTION_REPLACE_DELETE:
470                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->end_pos);
471                 gtk_stext_backward_delete(GTK_STEXT(undostruct->text), 
472                                           redoinfo->end_pos-redoinfo->start_pos);
473                 /* "pull" another data structure from the list */
474                 redoinfo = g_list_nth_data(undostruct->redo, 0);
475                 g_return_if_fail(redoinfo != NULL);
476                 undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
477                 undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
478                 g_return_if_fail(redoinfo->action==UNDO_ACTION_REPLACE_INSERT);
479                 gtk_stext_set_point(GTK_STEXT(undostruct->text), redoinfo->start_pos);
480                 gtk_stext_insert(GTK_STEXT(undostruct->text), NULL, NULL, 
481                                  NULL, redoinfo->text, -1);
482                 break;
483         case UNDO_ACTION_REPLACE_INSERT:
484                 g_warning("This should not happen. Redo: UNDO_REPLACE_INSERT");
485                 break;
486         default:
487                 g_assert_not_reached();
488                 break;
489         }
490
491         undostruct->change_func(undostruct, UNDO_STATE_TRUE, UNDO_STATE_UNCHANGED, 
492                                 undostruct->changewidget);
493                                 
494         if (g_list_length(undostruct->redo) == 0)
495                 undostruct->change_func(undostruct, UNDO_STATE_UNCHANGED, 
496                                         UNDO_STATE_FALSE, undostruct->changewidget);
497 }
498
499 void undo_insert_text_cb(GtkEditable *editable, gchar *new_text,
500                          gint new_text_length, gint *position, 
501                          UndoMain *undostruct) 
502 {
503         guchar *text_to_insert;
504
505         text_to_insert = g_strndup(new_text, new_text_length);
506         undo_add(text_to_insert, *position, (*position + new_text_length), 
507                  UNDO_ACTION_INSERT, undostruct);
508         g_free (text_to_insert);
509 }
510
511 void undo_delete_text_cb(GtkEditable *editable, gint start_pos,
512                          gint end_pos, UndoMain *undostruct) 
513 {
514         guchar *text_to_delete;
515
516         if (start_pos == end_pos )
517                 return;
518                 
519         text_to_delete = gtk_editable_get_chars(GTK_EDITABLE(editable), 
520                                                 start_pos, end_pos);
521         undo_add(text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE, undostruct);
522         g_free (text_to_delete);
523 }
524
525 void undo_paste_clipboard_cb (GtkEditable *editable, UndoMain *undostruct) 
526 {
527         debug_print("befor Paste: %d\n", undostruct->paste);
528         if (undo_get_selection(editable, NULL, NULL)) 
529                 undostruct->paste = TRUE;
530         debug_print("after Paste: %d\n", undostruct->paste);
531 }
532
533 /**
534  * undo_get_selection:
535  * @text: Text to get the selection from
536  * @start: return here the start position of the selection
537  * @end: return here the end position of the selection
538  *
539  * Gets the current selection for View
540  *
541  * Return Value: TRUE if there is a selection active, FALSE if not
542  **/
543 static gint undo_get_selection(GtkEditable *text, guint *start, guint *end) 
544 {
545         guint start_pos, end_pos;
546
547         start_pos = text->selection_start_pos;
548         end_pos   = text->selection_end_pos;
549
550         /* The user can select from end to start too. If so, swap it*/
551         if (end_pos < start_pos) {
552                 guint swap_pos;
553                 swap_pos  = end_pos;
554                 end_pos   = start_pos;
555                 start_pos = swap_pos;
556         }
557
558         if (start != NULL)
559                 *start = start_pos;
560                 
561         if (end != NULL)
562                 *end = end_pos;
563
564         if ((start_pos > 0 || end_pos > 0) && (start_pos != end_pos))
565                 return TRUE;
566         else
567                 return FALSE;
568 }