1 /*
2  * This program is free software; you can redistribute it and/or modify it
3  * under the terms of the GNU Lesser General Public License as published by
4  * the Free Software Foundation.
5  *
6  * This program is distributed in the hope that it will be useful, but
7  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
8  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
9  * for more details.
10  *
11  * You should have received a copy of the GNU Lesser General Public License
12  * along with this program; if not, see <http://www.gnu.org/licenses/>.
13  *
14  *
15  * Authors:
16  *		Milan Crha <mcrha@redhat.com>
17  *
18  * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
19  *
20  */
21 
22 #include "evolution-config.h"
23 
24 #include <gtk/gtk.h>
25 #include <glib/gi18n-lib.h>
26 
27 #include <string.h>
28 
29 #include "e-focus-tracker.h"
30 #include "e-widget-undo.h"
31 
32 #define DEFAULT_MAX_UNDO_LEVEL 256
33 #define UNDO_DATA_KEY "e-undo-data-ptr"
34 
35 /* calculates real index in EUndoData::undo_stack */
36 #define REAL_INDEX(x) ((data->undo_from + (x) + 2 * data->undo_len) % data->undo_len)
37 
38 typedef enum {
39 	E_UNDO_INSERT,
40 	E_UNDO_DELETE
41 } EUndoType;
42 
43 typedef enum {
44 	E_UNDO_DO_UNDO,
45 	E_UNDO_DO_REDO
46 } EUndoDoType;
47 
48 typedef struct _EUndoInfo {
49 	EUndoType type;
50 	gchar *text;
51 	gint position_start;
52 	gint position_end; /* valid for delete type only */
53 } EUndoInfo;
54 
55 typedef struct _EUndoData {
56 	EUndoInfo **undo_stack; /* stack for undo, with max_undo_level elements, some are NULL */
57 	gint undo_len; /* how many undo actions can be saved */
58 	gint undo_from; /* where the first undo action begins */
59 	gint n_undos; /* how many undo actions are saved;
60 		[(undo_from + n_undos) % undo_len] is the next free undo item (or the first redo) */
61 	gint n_redos; /* how many redo actions are saved */
62 
63 	EUndoInfo *current_info; /* the top undo action */
64 
65 	gulong insert_handler_id;
66 	gulong delete_handler_id;
67 } EUndoData;
68 
69 static void
free_undo_info(gpointer ptr)70 free_undo_info (gpointer ptr)
71 {
72 	EUndoInfo *info = ptr;
73 
74 	if (info) {
75 		g_free (info->text);
76 		g_free (info);
77 	}
78 }
79 
80 static void
free_undo_data(gpointer ptr)81 free_undo_data (gpointer ptr)
82 {
83 	EUndoData *data = ptr;
84 
85 	if (data) {
86 		gint ii;
87 
88 		for (ii = 0; ii < data->undo_len; ii++) {
89 			free_undo_info (data->undo_stack[ii]);
90 		}
91 		g_free (data->undo_stack);
92 		g_free (data);
93 	}
94 }
95 
96 static void
reset_redos(EUndoData * data)97 reset_redos (EUndoData *data)
98 {
99 	gint ii, index;
100 
101 	for (ii = 0; ii < data->n_redos; ii++) {
102 		index = REAL_INDEX (data->n_undos + ii);
103 
104 		free_undo_info (data->undo_stack[index]);
105 		data->undo_stack[index] = NULL;
106 	}
107 
108 	data->n_redos = 0;
109 }
110 
111 static void
push_undo(EUndoData * data,EUndoInfo * info)112 push_undo (EUndoData *data,
113            EUndoInfo *info)
114 {
115 	gint index;
116 
117 	reset_redos (data);
118 
119 	if (data->n_undos == data->undo_len) {
120 		data->undo_from = (data->undo_from + 1) % data->undo_len;
121 	} else {
122 		data->n_undos++;
123 	}
124 
125 	index = REAL_INDEX (data->n_undos - 1);
126 	free_undo_info (data->undo_stack[index]);
127 	data->undo_stack[index] = info;
128 }
129 
130 static gboolean
can_merge_insert_undos(EUndoInfo * current_info,const gchar * text,gint text_len,gint position)131 can_merge_insert_undos (EUndoInfo *current_info,
132                         const gchar *text,
133                         gint text_len,
134                         gint position)
135 {
136 	gint len;
137 
138 	/* allow only one letter merge */
139 	if (!current_info || current_info->type != E_UNDO_INSERT ||
140 	    !text || text_len <= 0 || text_len > 1)
141 		return FALSE;
142 
143 	if (text[0] == '\r' || text[0] == '\n')
144 		return FALSE;
145 
146 	len = strlen (current_info->text);
147 	if (position != current_info->position_start + len)
148 		return FALSE;
149 
150 	if (g_ascii_isspace (text[0])) {
151 		if (len <= 0 || !g_ascii_isspace (current_info->text[len - 1]))
152 			return FALSE;
153 	}
154 
155 	return TRUE;
156 }
157 
158 static void
push_insert_undo(GObject * object,const gchar * text,gint text_len,gint position)159 push_insert_undo (GObject *object,
160                   const gchar *text,
161                   gint text_len,
162                   gint position)
163 {
164 	EUndoData *data;
165 	EUndoInfo *info;
166 
167 	data = g_object_get_data (object, UNDO_DATA_KEY);
168 	if (!data) {
169 		g_warn_if_reached ();
170 		return;
171 	}
172 
173 	/* one letter long text, divide undos on spaces */
174 	if (data->current_info &&
175 	    can_merge_insert_undos (data->current_info, text, text_len, position)) {
176 		gchar *new_text;
177 
178 		new_text = g_strdup_printf ("%s%*s", data->current_info->text, text_len, text);
179 		g_free (data->current_info->text);
180 		data->current_info->text = new_text;
181 
182 		return;
183 	}
184 
185 	info = g_new0 (EUndoInfo, 1);
186 	info->type = E_UNDO_INSERT;
187 	info->text = g_strndup (text, text_len);
188 	info->position_start = position;
189 
190 	push_undo (data, info);
191 
192 	data->current_info = info;
193 }
194 
195 static void
push_delete_undo(GObject * object,gchar * text,gint position_start,gint position_end)196 push_delete_undo (GObject *object,
197                   gchar *text, /* takes ownership of the 'text' */
198                   gint position_start,
199                   gint position_end)
200 {
201 	EUndoData *data;
202 	EUndoInfo *info;
203 
204 	data = g_object_get_data (object, UNDO_DATA_KEY);
205 	if (!data) {
206 		g_warn_if_reached ();
207 		return;
208 	}
209 
210 	if (data->current_info && data->current_info->type == E_UNDO_DELETE &&
211 	    position_end - position_start == 1 && !g_ascii_isspace (*text)) {
212 		info = data->current_info;
213 
214 		if (info->position_start == position_start) {
215 			gchar *new_text;
216 
217 			new_text = g_strconcat (info->text, text, NULL);
218 			g_free (info->text);
219 			info->text = new_text;
220 			g_free (text);
221 
222 			info->position_end++;
223 
224 			return;
225 		} else if (data->current_info->position_start == position_end) {
226 			gchar *new_text;
227 
228 			new_text = g_strconcat (text, info->text, NULL);
229 			g_free (info->text);
230 			info->text = new_text;
231 			g_free (text);
232 
233 			info->position_start = position_start;
234 
235 			return;
236 		}
237 	}
238 
239 	info = g_new0 (EUndoInfo, 1);
240 	info->type = E_UNDO_DELETE;
241 	info->text = text;
242 	info->position_start = position_start;
243 	info->position_end = position_end;
244 
245 	push_undo (data, info);
246 
247 	data->current_info = info;
248 }
249 
250 static void
editable_undo_insert_text_cb(GtkEditable * editable,gchar * text,gint text_length,gint * position,gpointer user_data)251 editable_undo_insert_text_cb (GtkEditable *editable,
252                               gchar *text,
253                               gint text_length,
254                               gint *position,
255                               gpointer user_data)
256 {
257 	push_insert_undo (G_OBJECT (editable), text, text_length, *position);
258 }
259 
260 static void
editable_undo_delete_text_cb(GtkEditable * editable,gint start_pos,gint end_pos,gpointer user_data)261 editable_undo_delete_text_cb (GtkEditable *editable,
262                               gint start_pos,
263                               gint end_pos,
264                               gpointer user_data)
265 {
266 	push_delete_undo (G_OBJECT (editable), gtk_editable_get_chars (editable, start_pos, end_pos), start_pos, end_pos);
267 }
268 
269 static void
editable_undo_insert_text(GObject * object,const gchar * text,gint position)270 editable_undo_insert_text (GObject *object,
271                            const gchar *text,
272                            gint position)
273 {
274 	g_return_if_fail (GTK_IS_EDITABLE (object));
275 
276 	gtk_editable_insert_text (GTK_EDITABLE (object), text, -1, &position);
277 }
278 
279 static void
editable_undo_delete_text(GObject * object,gint position_start,gint position_end)280 editable_undo_delete_text (GObject *object,
281                            gint position_start,
282                            gint position_end)
283 {
284 	g_return_if_fail (GTK_IS_EDITABLE (object));
285 
286 	gtk_editable_delete_text (GTK_EDITABLE (object), position_start, position_end);
287 }
288 
289 static void
text_buffer_undo_insert_text_cb(GtkTextBuffer * text_buffer,GtkTextIter * location,gchar * text,gint text_length,gpointer user_data)290 text_buffer_undo_insert_text_cb (GtkTextBuffer *text_buffer,
291                                  GtkTextIter *location,
292                                  gchar *text,
293                                  gint text_length,
294                                  gpointer user_data)
295 {
296 	push_insert_undo (G_OBJECT (text_buffer), text, text_length, gtk_text_iter_get_offset (location));
297 }
298 
299 static void
text_buffer_undo_delete_range_cb(GtkTextBuffer * text_buffer,GtkTextIter * start,GtkTextIter * end,gpointer user_data)300 text_buffer_undo_delete_range_cb (GtkTextBuffer *text_buffer,
301                                   GtkTextIter *start,
302                                   GtkTextIter *end,
303                                   gpointer user_data)
304 {
305 	push_delete_undo (
306 		G_OBJECT (text_buffer),
307 		gtk_text_iter_get_text (start, end),
308 		gtk_text_iter_get_offset (start),
309 		gtk_text_iter_get_offset (end));
310 }
311 
312 static void
text_buffer_undo_insert_text(GObject * object,const gchar * text,gint position)313 text_buffer_undo_insert_text (GObject *object,
314                               const gchar *text,
315                               gint position)
316 {
317 	GtkTextBuffer *text_buffer;
318 	GtkTextIter iter;
319 
320 	g_return_if_fail (GTK_IS_TEXT_BUFFER (object));
321 
322 	text_buffer = GTK_TEXT_BUFFER (object);
323 
324 	gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, position);
325 	gtk_text_buffer_insert (text_buffer, &iter, text, -1);
326 }
327 
328 static void
text_buffer_undo_delete_text(GObject * object,gint position_start,gint position_end)329 text_buffer_undo_delete_text (GObject *object,
330                               gint position_start,
331                               gint position_end)
332 {
333 	GtkTextBuffer *text_buffer;
334 	GtkTextIter start_iter, end_iter;
335 
336 	g_return_if_fail (GTK_IS_TEXT_BUFFER (object));
337 
338 	text_buffer = GTK_TEXT_BUFFER (object);
339 
340 	gtk_text_buffer_get_iter_at_offset (text_buffer, &start_iter, position_start);
341 	gtk_text_buffer_get_iter_at_offset (text_buffer, &end_iter, position_end);
342 	gtk_text_buffer_delete (text_buffer, &start_iter, &end_iter);
343 }
344 
345 static void
widget_undo_place_cursor_at(GObject * object,gint char_pos)346 widget_undo_place_cursor_at (GObject *object,
347                              gint char_pos)
348 {
349 	if (GTK_IS_EDITABLE (object))
350 		gtk_editable_set_position (GTK_EDITABLE (object), char_pos);
351 	else if (GTK_IS_TEXT_BUFFER (object)) {
352 		GtkTextBuffer *buffer;
353 		GtkTextIter pos;
354 
355 		buffer = GTK_TEXT_BUFFER (object);
356 
357 		gtk_text_buffer_get_iter_at_offset (buffer, &pos, char_pos);
358 		gtk_text_buffer_place_cursor (buffer, &pos);
359 	}
360 }
361 
362 static void
undo_do_something(GObject * object,EUndoDoType todo,void (* insert_func)(GObject * object,const gchar * text,gint position),void (* delete_func)(GObject * object,gint position_start,gint position_end))363 undo_do_something (GObject *object,
364                    EUndoDoType todo,
365                    void (* insert_func) (GObject *object,
366                    const gchar *text,
367                    gint position),
368                    void (* delete_func) (GObject *object,
369                    gint position_start,
370                    gint position_end))
371 {
372 	EUndoData *data;
373 	EUndoInfo *info = NULL;
374 
375 	data = g_object_get_data (object, UNDO_DATA_KEY);
376 	if (!data)
377 		return;
378 
379 	if (todo == E_UNDO_DO_UNDO && data->n_undos > 0) {
380 		info = data->undo_stack[REAL_INDEX (data->n_undos - 1)];
381 		data->n_undos--;
382 		data->n_redos++;
383 	} else if (todo == E_UNDO_DO_REDO && data->n_redos > 0) {
384 		info = data->undo_stack[REAL_INDEX (data->n_undos)];
385 		data->n_undos++;
386 		data->n_redos--;
387 	}
388 
389 	if (!info)
390 		return;
391 
392 	g_signal_handler_block (object, data->insert_handler_id);
393 	g_signal_handler_block (object, data->delete_handler_id);
394 
395 	if (info->type == E_UNDO_INSERT) {
396 		if (todo == E_UNDO_DO_UNDO) {
397 			delete_func (object, info->position_start, info->position_start + g_utf8_strlen (info->text, -1));
398 			widget_undo_place_cursor_at (object, info->position_start);
399 		} else {
400 			insert_func (object, info->text, info->position_start);
401 			widget_undo_place_cursor_at (object, info->position_start + g_utf8_strlen (info->text, -1));
402 		}
403 	} else if (info->type == E_UNDO_DELETE) {
404 		if (todo == E_UNDO_DO_UNDO) {
405 			insert_func (object, info->text, info->position_start);
406 			widget_undo_place_cursor_at (object, info->position_start + g_utf8_strlen (info->text, -1));
407 		} else {
408 			delete_func (object, info->position_start, info->position_end);
409 			widget_undo_place_cursor_at (object, info->position_start);
410 		}
411 	}
412 
413 	data->current_info = NULL;
414 
415 	g_signal_handler_unblock (object, data->delete_handler_id);
416 	g_signal_handler_unblock (object, data->insert_handler_id);
417 }
418 
419 static gchar *
undo_describe_info(EUndoInfo * info,EUndoDoType undo_type)420 undo_describe_info (EUndoInfo *info,
421                     EUndoDoType undo_type)
422 {
423 	if (!info)
424 		return NULL;
425 
426 	if (info->type == E_UNDO_INSERT) {
427 		if (undo_type == E_UNDO_DO_UNDO)
428 			return g_strdup (_("Undo “Insert text”"));
429 		else
430 			return g_strdup (_("Redo “Insert text”"));
431 		/* if (strlen (info->text) > 15) {
432 			if (undo_type == E_UNDO_DO_UNDO)
433 				return g_strdup_printf (_("Undo “Insert “%.12s...””"), info->text);
434 			else
435 				return g_strdup_printf (_("Redo “Insert “%.12s...””"), info->text);
436 		}
437  *
438 		if (undo_type == E_UNDO_DO_UNDO)
439 			return g_strdup_printf (_("Undo “Insert “%s””"), info->text);
440 		else
441 			return g_strdup_printf (_("Redo “Insert “%s””"), info->text); */
442 	} else if (info->type == E_UNDO_DELETE) {
443 		if (undo_type == E_UNDO_DO_UNDO)
444 			return g_strdup (_("Undo “Delete text”"));
445 		else
446 			return g_strdup (_("Redo “Delete text”"));
447 		/* if (strlen (info->text) > 15) {
448 			if (undo_type == E_UNDO_DO_UNDO)
449 				return g_strdup_printf (_("Undo “Delete “%.12s...””"), info->text);
450 			else
451 				return g_strdup_printf (_("Redo “Delete “%.12s...””"), info->text);
452 		}
453  *
454 		if (undo_type == E_UNDO_DO_UNDO)
455 			return g_strdup_printf (_("Undo “Delete “%s””"), info->text);
456 		else
457 			return g_strdup_printf (_("Redo “Delete “%s””"), info->text); */
458 	}
459 
460 	return NULL;
461 }
462 
463 static gboolean
undo_check_undo(GObject * object,gchar ** description)464 undo_check_undo (GObject *object,
465                  gchar **description)
466 {
467 	EUndoData *data;
468 
469 	data = g_object_get_data (object, UNDO_DATA_KEY);
470 	if (!data)
471 		return FALSE;
472 
473 	if (data->n_undos <= 0)
474 		return FALSE;
475 
476 	if (description)
477 		*description = undo_describe_info (data->undo_stack[REAL_INDEX (data->n_undos - 1)], E_UNDO_DO_UNDO);
478 
479 	return TRUE;
480 }
481 
482 static gboolean
undo_check_redo(GObject * object,gchar ** description)483 undo_check_redo (GObject *object,
484                  gchar **description)
485 {
486 	EUndoData *data;
487 
488 	data = g_object_get_data (object, UNDO_DATA_KEY);
489 	if (!data)
490 		return FALSE;
491 
492 	if (data->n_redos <= 0)
493 		return FALSE;
494 
495 	if (description)
496 		*description = undo_describe_info (data->undo_stack[REAL_INDEX (data->n_undos)], E_UNDO_DO_REDO);
497 
498 	return TRUE;
499 }
500 
501 static void
undo_reset(GObject * object)502 undo_reset (GObject *object)
503 {
504 	EUndoData *data;
505 
506 	data = g_object_get_data (object, UNDO_DATA_KEY);
507 	if (!data)
508 		return;
509 
510 	data->n_undos = 0;
511 	data->n_redos = 0;
512 	data->current_info = NULL;
513 }
514 
515 static void
widget_undo_popup_activate_cb(GObject * menu_item,GtkWidget * widget)516 widget_undo_popup_activate_cb (GObject *menu_item,
517                                GtkWidget *widget)
518 {
519 	EUndoDoType undo_type = GPOINTER_TO_INT (g_object_get_data (menu_item, UNDO_DATA_KEY));
520 
521 	if (undo_type == E_UNDO_DO_UNDO)
522 		e_widget_undo_do_undo (widget);
523 	else
524 		e_widget_undo_do_redo (widget);
525 }
526 
527 static gboolean
widget_undo_prepend_popup(GtkWidget * widget,GtkMenuShell * menu,EUndoDoType undo_type,gboolean already_added)528 widget_undo_prepend_popup (GtkWidget *widget,
529                            GtkMenuShell *menu,
530                            EUndoDoType undo_type,
531                            gboolean already_added)
532 {
533 	gchar *description = NULL;
534 	const gchar *icon_name = NULL;
535 
536 	if (undo_type == E_UNDO_DO_UNDO && e_widget_undo_has_undo (widget)) {
537 		description = e_widget_undo_describe_undo (widget);
538 		icon_name = "edit-undo";
539 	} else if (undo_type == E_UNDO_DO_REDO && e_widget_undo_has_redo (widget)) {
540 		description = e_widget_undo_describe_redo (widget);
541 		icon_name = "edit-redo";
542 	}
543 
544 	if (description) {
545 		GtkWidget *item, *image;
546 
547 		if (!already_added) {
548 			item = gtk_separator_menu_item_new ();
549 			gtk_widget_show (item);
550 			gtk_menu_shell_prepend (menu, item);
551 
552 			already_added = TRUE;
553 		}
554 
555 		image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
556 		item = gtk_image_menu_item_new_with_label (description);
557 		gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
558 		gtk_widget_show (item);
559 
560 		g_object_set_data (G_OBJECT (item), UNDO_DATA_KEY, GINT_TO_POINTER (undo_type));
561 		g_signal_connect (item, "activate", G_CALLBACK (widget_undo_popup_activate_cb), widget);
562 
563 		gtk_menu_shell_prepend (menu, item);
564 
565 		g_free (description);
566 	}
567 
568 	return already_added;
569 }
570 
571 static void
widget_undo_populate_popup_cb(GtkWidget * widget,GtkWidget * popup,gpointer user_data)572 widget_undo_populate_popup_cb (GtkWidget *widget,
573                                GtkWidget *popup,
574                                gpointer user_data)
575 {
576 	GtkMenuShell *menu;
577 	gboolean added = FALSE;
578 
579 	if (!GTK_IS_MENU (popup))
580 		return;
581 
582 	menu = GTK_MENU_SHELL (popup);
583 
584 	/* first redo, because prependend, thus undo gets before it */
585 	if (e_widget_undo_has_redo (widget))
586 		added = widget_undo_prepend_popup (widget, menu, E_UNDO_DO_REDO, added);
587 
588 	if (e_widget_undo_has_undo (widget))
589 		widget_undo_prepend_popup (widget, menu, E_UNDO_DO_UNDO, added);
590 }
591 
592 /**
593  * e_widget_undo_attach:
594  * @widget: a #GtkWidget, where to attach undo functionality
595  * @focus_tracker: an #EFocusTracker, can be %NULL
596  *
597  * The function does nothing, if the widget is not of a supported type
598  * for undo functionality, same as when the undo is already attached.
599  * It is ensured that the actions of the provided @focus_tracker are
600  * updated on change of the @widget.
601  *
602  * See @e_widget_undo_is_attached().
603  *
604  * Since: 3.12
605  **/
606 void
e_widget_undo_attach(GtkWidget * widget,EFocusTracker * focus_tracker)607 e_widget_undo_attach (GtkWidget *widget,
608                       EFocusTracker *focus_tracker)
609 {
610 	EUndoData *data;
611 
612 	if (e_widget_undo_is_attached (widget))
613 		return;
614 
615 	if (GTK_IS_EDITABLE (widget)) {
616 		data = g_new0 (EUndoData, 1);
617 		data->undo_len = DEFAULT_MAX_UNDO_LEVEL;
618 		data->undo_stack = g_new0 (EUndoInfo *, data->undo_len);
619 
620 		g_object_set_data_full (G_OBJECT (widget), UNDO_DATA_KEY, data, free_undo_data);
621 
622 		data->insert_handler_id = g_signal_connect (
623 			widget, "insert-text",
624 			G_CALLBACK (editable_undo_insert_text_cb), NULL);
625 		data->delete_handler_id = g_signal_connect (
626 			widget, "delete-text",
627 			G_CALLBACK (editable_undo_delete_text_cb), NULL);
628 
629 		if (focus_tracker)
630 			g_signal_connect_swapped (
631 				widget, "changed",
632 				G_CALLBACK (e_focus_tracker_update_actions), focus_tracker);
633 
634 		if (GTK_IS_ENTRY (widget))
635 			g_signal_connect (
636 				widget, "populate-popup",
637 				G_CALLBACK (widget_undo_populate_popup_cb), NULL);
638 	} else if (GTK_IS_TEXT_VIEW (widget)) {
639 		GtkTextBuffer *text_buffer;
640 
641 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
642 
643 		data = g_new0 (EUndoData, 1);
644 		data->undo_len = DEFAULT_MAX_UNDO_LEVEL;
645 		data->undo_stack = g_new0 (EUndoInfo *, data->undo_len);
646 
647 		g_object_set_data_full (G_OBJECT (text_buffer), UNDO_DATA_KEY, data, free_undo_data);
648 
649 		data->insert_handler_id = g_signal_connect (
650 			text_buffer, "insert-text",
651 			G_CALLBACK (text_buffer_undo_insert_text_cb), NULL);
652 		data->delete_handler_id = g_signal_connect (
653 			text_buffer, "delete-range",
654 			G_CALLBACK (text_buffer_undo_delete_range_cb), NULL);
655 
656 		if (focus_tracker)
657 			g_signal_connect_swapped (
658 				text_buffer, "changed",
659 				G_CALLBACK (e_focus_tracker_update_actions), focus_tracker);
660 
661 		g_signal_connect (
662 			widget, "populate-popup",
663 			G_CALLBACK (widget_undo_populate_popup_cb), NULL);
664 	}
665 }
666 
667 /**
668  * e_widget_undo_is_attached:
669  * @widget: a #GtkWidget, where to test whether undo functionality is attached.
670  *
671  * Checks whether the given widget has already attached an undo
672  * functionality - it is done with @e_widget_undo_attach().
673  *
674  * Returns: Whether the given @widget has already attached undo functionality.
675  *
676  * Since: 3.12
677  **/
678 gboolean
e_widget_undo_is_attached(GtkWidget * widget)679 e_widget_undo_is_attached (GtkWidget *widget)
680 {
681 	gboolean res = FALSE;
682 
683 	if (GTK_IS_EDITABLE (widget)) {
684 		res = g_object_get_data (G_OBJECT (widget), UNDO_DATA_KEY) != NULL;
685 	} else if (GTK_IS_TEXT_VIEW (widget)) {
686 		GtkTextBuffer *text_buffer;
687 
688 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
689 
690 		res = g_object_get_data (G_OBJECT (text_buffer), UNDO_DATA_KEY) != NULL;
691 	}
692 
693 	return res;
694 }
695 
696 /**
697  * e_widget_undo_has_undo:
698  * @widget: a #GtkWidget
699  *
700  * Returns: Whether the given @widget has any undo available.
701  *
702  * See: @e_widget_undo_describe_undo, @e_widget_undo_do_undo
703  *
704  * Since: 3.12
705  **/
706 gboolean
e_widget_undo_has_undo(GtkWidget * widget)707 e_widget_undo_has_undo (GtkWidget *widget)
708 {
709 	if (GTK_IS_EDITABLE (widget)) {
710 		return undo_check_undo (G_OBJECT (widget), NULL);
711 	} else if (GTK_IS_TEXT_VIEW (widget)) {
712 		GtkTextBuffer *text_buffer;
713 
714 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
715 
716 		return undo_check_undo (G_OBJECT (text_buffer), NULL);
717 	}
718 
719 	return FALSE;
720 }
721 
722 /**
723  * e_widget_undo_has_redo:
724  * @widget: a #GtkWidget
725  *
726  * Returns: Whether the given @widget has any redo available.
727  *
728  * See: @e_widget_undo_describe_redo, @e_widget_undo_do_redo
729  *
730  * Since: 3.12
731  **/
732 gboolean
e_widget_undo_has_redo(GtkWidget * widget)733 e_widget_undo_has_redo (GtkWidget *widget)
734 {
735 	if (GTK_IS_EDITABLE (widget)) {
736 		return undo_check_redo (G_OBJECT (widget), NULL);
737 	} else if (GTK_IS_TEXT_VIEW (widget)) {
738 		GtkTextBuffer *text_buffer;
739 
740 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
741 
742 		return undo_check_redo (G_OBJECT (text_buffer), NULL);
743 	}
744 
745 	return FALSE;
746 }
747 
748 /**
749  * e_widget_undo_describe_undo:
750  * @widget: a #GtkWidget
751  *
752  * Returns: (transfer full): Description of a top undo action available
753  *    for the @widget, %NULL when there is no undo action. Returned pointer,
754  *    if not %NULL, should be freed with g_free().
755  *
756  * See: @e_widget_undo_has_undo, @e_widget_undo_do_undo
757  *
758  * Since: 3.12
759  **/
760 gchar *
e_widget_undo_describe_undo(GtkWidget * widget)761 e_widget_undo_describe_undo (GtkWidget *widget)
762 {
763 	gchar *res = NULL;
764 
765 	if (GTK_IS_EDITABLE (widget)) {
766 		if (!undo_check_undo (G_OBJECT (widget), &res)) {
767 			g_warn_if_fail (res == NULL);
768 		}
769 	} else if (GTK_IS_TEXT_VIEW (widget)) {
770 		GtkTextBuffer *text_buffer;
771 
772 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
773 
774 		if (!undo_check_undo (G_OBJECT (text_buffer), &res)) {
775 			g_warn_if_fail (res == NULL);
776 		}
777 	}
778 
779 	return res;
780 }
781 
782 /**
783  * e_widget_undo_describe_redo:
784  * @widget: a #GtkWidget
785  *
786  * Returns: (transfer full): Description of a top redo action available
787  *    for the @widget, %NULL when there is no redo action. Returned pointer,
788  *    if not %NULL, should be freed with g_free().
789  *
790  * See: @e_widget_undo_has_redo, @e_widget_undo_do_redo
791  *
792  * Since: 3.12
793  **/
794 gchar *
e_widget_undo_describe_redo(GtkWidget * widget)795 e_widget_undo_describe_redo (GtkWidget *widget)
796 {
797 	gchar *res = NULL;
798 
799 	if (GTK_IS_EDITABLE (widget)) {
800 		if (!undo_check_redo (G_OBJECT (widget), &res)) {
801 			g_warn_if_fail (res == NULL);
802 		}
803 	} else if (GTK_IS_TEXT_VIEW (widget)) {
804 		GtkTextBuffer *text_buffer;
805 
806 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
807 
808 		if (!undo_check_redo (G_OBJECT (text_buffer), &res)) {
809 			g_warn_if_fail (res == NULL);
810 		}
811 	}
812 
813 	return res;
814 }
815 
816 /**
817  * e_widget_undo_do_undo:
818  * @widget: a #GtkWidget
819  *
820  * Applies the top undo action on the @widget, which also remembers
821  * a redo action. It does nothing if the widget doesn't have
822  * attached undo functionality (@e_widget_undo_attach()), neither
823  * when there is no undo action available.
824  *
825  * See: @e_widget_undo_attach, @e_widget_undo_has_undo, @e_widget_undo_describe_undo
826  *
827  * Since: 3.12
828  **/
829 void
e_widget_undo_do_undo(GtkWidget * widget)830 e_widget_undo_do_undo (GtkWidget *widget)
831 {
832 	if (GTK_IS_EDITABLE (widget)) {
833 		undo_do_something (
834 			G_OBJECT (widget),
835 			E_UNDO_DO_UNDO,
836 			editable_undo_insert_text,
837 			editable_undo_delete_text);
838 	} else if (GTK_IS_TEXT_VIEW (widget)) {
839 		GtkTextBuffer *text_buffer;
840 
841 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
842 
843 		undo_do_something (
844 			G_OBJECT (text_buffer),
845 			E_UNDO_DO_UNDO,
846 			text_buffer_undo_insert_text,
847 			text_buffer_undo_delete_text);
848 	}
849 }
850 
851 /**
852  * e_widget_undo_do_redo:
853  * @widget: a #GtkWidget
854  *
855  * Applies the top redo action on the @widget, which also remembers
856  * an undo action. It does nothing if the widget doesn't have
857  * attached undo functionality (@e_widget_undo_attach()), neither
858  * when there is no redo action available.
859  *
860  * See: @e_widget_undo_attach, @e_widget_undo_has_redo, @e_widget_undo_describe_redo
861  *
862  * Since: 3.12
863  **/
864 void
e_widget_undo_do_redo(GtkWidget * widget)865 e_widget_undo_do_redo (GtkWidget *widget)
866 {
867 	if (GTK_IS_EDITABLE (widget)) {
868 		undo_do_something (
869 			G_OBJECT (widget),
870 			E_UNDO_DO_REDO,
871 			editable_undo_insert_text,
872 			editable_undo_delete_text);
873 	} else if (GTK_IS_TEXT_VIEW (widget)) {
874 		GtkTextBuffer *text_buffer;
875 
876 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
877 
878 		undo_do_something (
879 			G_OBJECT (text_buffer),
880 			E_UNDO_DO_REDO,
881 			text_buffer_undo_insert_text,
882 			text_buffer_undo_delete_text);
883 	}
884 }
885 
886 /**
887  * e_widget_undo_reset:
888  * @widget: a #GtkWidget, on which might be attached undo functionality
889  *
890  * Resets undo and redo stack to empty on a widget with attached
891  * undo functionality. It does nothing, if the widget does not have
892  * the undo functionality attached (see @e_widget_undo_attach()).
893  *
894  * Since: 3.12
895  **/
896 void
e_widget_undo_reset(GtkWidget * widget)897 e_widget_undo_reset (GtkWidget *widget)
898 {
899 	if (GTK_IS_EDITABLE (widget)) {
900 		undo_reset (G_OBJECT (widget));
901 	} else if (GTK_IS_TEXT_VIEW (widget)) {
902 		GtkTextBuffer *text_buffer;
903 
904 		text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
905 
906 		undo_reset (G_OBJECT (text_buffer));
907 	}
908 }
909