aboutsummaryrefslogtreecommitdiffstats
path: root/e-util
diff options
context:
space:
mode:
authorMilan Crha <mcrha@redhat.com>2014-01-11 00:18:49 +0800
committerMilan Crha <mcrha@redhat.com>2014-01-11 00:18:49 +0800
commit63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c (patch)
tree4af77bd941992bb173dac1e4480f51fb30fb5b1a /e-util
parent04ed82b0530ca7fa34008876b056378dff6b76fb (diff)
downloadgsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.tar
gsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.tar.gz
gsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.tar.bz2
gsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.tar.lz
gsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.tar.xz
gsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.tar.zst
gsoc2013-evolution-63a1f0eab3e15e0d64e24bd5a2659a61347cfe9c.zip
Bug #333184 - Add Undo support to component editors
Diffstat (limited to 'e-util')
-rw-r--r--e-util/Makefile.am2
-rw-r--r--e-util/e-focus-tracker.c365
-rw-r--r--e-util/e-focus-tracker.h8
-rw-r--r--e-util/e-selectable.c26
-rw-r--r--e-util/e-selectable.h4
-rw-r--r--e-util/e-util.h1
-rw-r--r--e-util/e-widget-undo.c891
-rw-r--r--e-util/e-widget-undo.h48
8 files changed, 1325 insertions, 20 deletions
diff --git a/e-util/Makefile.am b/e-util/Makefile.am
index cf13b394d6..e6321db999 100644
--- a/e-util/Makefile.am
+++ b/e-util/Makefile.am
@@ -307,6 +307,7 @@ evolution_util_include_HEADERS = \
e-web-view-gtkhtml.h \
e-web-view-preview.h \
e-web-view.h \
+ e-widget-undo.h \
e-xml-utils.h \
ea-calendar-cell.h \
ea-calendar-item.h \
@@ -548,6 +549,7 @@ libevolution_util_la_SOURCES = \
e-web-view-gtkhtml.c \
e-web-view-preview.c \
e-web-view.c \
+ e-widget-undo.c \
e-xml-utils.c \
ea-calendar-cell.c \
ea-calendar-item.c \
diff --git a/e-util/e-focus-tracker.c b/e-util/e-focus-tracker.c
index 958eda87a6..36d2421711 100644
--- a/e-util/e-focus-tracker.c
+++ b/e-util/e-focus-tracker.c
@@ -27,6 +27,7 @@
#include <glib/gi18n-lib.h>
#include "e-selectable.h"
+#include "e-widget-undo.h"
#define E_FOCUS_TRACKER_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE \
@@ -41,6 +42,8 @@ struct _EFocusTrackerPrivate {
GtkAction *paste_clipboard;
GtkAction *delete_selection;
GtkAction *select_all;
+ GtkAction *undo;
+ GtkAction *redo;
};
enum {
@@ -51,7 +54,9 @@ enum {
PROP_COPY_CLIPBOARD_ACTION,
PROP_PASTE_CLIPBOARD_ACTION,
PROP_DELETE_SELECTION_ACTION,
- PROP_SELECT_ALL_ACTION
+ PROP_SELECT_ALL_ACTION,
+ PROP_UNDO_ACTION,
+ PROP_REDO_ACTION
};
G_DEFINE_TYPE (
@@ -83,6 +88,55 @@ focus_tracker_disable_actions (EFocusTracker *focus_tracker)
action = e_focus_tracker_get_select_all_action (focus_tracker);
if (action != NULL)
gtk_action_set_sensitive (action, FALSE);
+
+ action = e_focus_tracker_get_undo_action (focus_tracker);
+ if (action != NULL)
+ gtk_action_set_sensitive (action, FALSE);
+
+ action = e_focus_tracker_get_redo_action (focus_tracker);
+ if (action != NULL)
+ gtk_action_set_sensitive (action, FALSE);
+}
+
+static void
+focus_tracker_update_undo_redo (EFocusTracker *focus_tracker,
+ GtkWidget *widget,
+ gboolean can_edit_text)
+{
+ GtkAction *action;
+ gboolean sensitive;
+
+ action = e_focus_tracker_get_undo_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = can_edit_text && widget && e_widget_undo_has_undo (widget);
+ gtk_action_set_sensitive (action, sensitive);
+
+ if (sensitive) {
+ gchar *description;
+
+ description = e_widget_undo_describe_undo (widget);
+ gtk_action_set_tooltip (action, description);
+ g_free (description);
+ } else {
+ gtk_action_set_tooltip (action, _("Undo"));
+ }
+ }
+
+ action = e_focus_tracker_get_redo_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = can_edit_text && widget && e_widget_undo_has_redo (widget);
+ gtk_action_set_sensitive (action, sensitive);
+
+ if (sensitive) {
+ gchar *description;
+
+ description = e_widget_undo_describe_redo (widget);
+ gtk_action_set_tooltip (action, description);
+ g_free (description);
+ } else {
+ gtk_action_set_tooltip (action, _("Redo"));
+ }
+ }
}
static void
@@ -140,6 +194,65 @@ focus_tracker_editable_update_actions (EFocusTracker *focus_tracker,
gtk_action_set_sensitive (action, sensitive);
gtk_action_set_tooltip (action, _("Select all text"));
}
+
+ focus_tracker_update_undo_redo (focus_tracker, GTK_WIDGET (editable), can_edit_text);
+}
+
+
+static void
+focus_tracker_text_view_update_actions (EFocusTracker *focus_tracker,
+ GtkTextView *text_view,
+ GdkAtom *targets,
+ gint n_targets)
+{
+ GtkAction *action;
+ GtkTextBuffer *buffer;
+ gboolean can_edit_text;
+ gboolean clipboard_has_text;
+ gboolean text_is_selected;
+ gboolean sensitive;
+
+ buffer = gtk_text_view_get_buffer (text_view);
+ can_edit_text = gtk_text_view_get_editable (text_view);
+ clipboard_has_text = (targets != NULL) && gtk_targets_include_text (targets, n_targets);
+ text_is_selected = gtk_text_buffer_get_selection_bounds (buffer, NULL, NULL);
+
+ action = e_focus_tracker_get_cut_clipboard_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = can_edit_text && text_is_selected;
+ gtk_action_set_sensitive (action, sensitive);
+ gtk_action_set_tooltip (action, _("Cut the selection"));
+ }
+
+ action = e_focus_tracker_get_copy_clipboard_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = text_is_selected;
+ gtk_action_set_sensitive (action, sensitive);
+ gtk_action_set_tooltip (action, _("Copy the selection"));
+ }
+
+ action = e_focus_tracker_get_paste_clipboard_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = can_edit_text && clipboard_has_text;
+ gtk_action_set_sensitive (action, sensitive);
+ gtk_action_set_tooltip (action, _("Paste the clipboard"));
+ }
+
+ action = e_focus_tracker_get_delete_selection_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = can_edit_text && text_is_selected;
+ gtk_action_set_sensitive (action, sensitive);
+ gtk_action_set_tooltip (action, _("Delete the selection"));
+ }
+
+ action = e_focus_tracker_get_select_all_action (focus_tracker);
+ if (action != NULL) {
+ sensitive = TRUE; /* always enabled */
+ gtk_action_set_sensitive (action, sensitive);
+ gtk_action_set_tooltip (action, _("Select all text"));
+ }
+
+ focus_tracker_update_undo_redo (focus_tracker, GTK_WIDGET (text_view), can_edit_text);
}
static void
@@ -181,6 +294,14 @@ focus_tracker_selectable_update_actions (EFocusTracker *focus_tracker,
action = e_focus_tracker_get_select_all_action (focus_tracker);
if (action != NULL && interface->select_all == NULL)
gtk_action_set_sensitive (action, FALSE);
+
+ action = e_focus_tracker_get_undo_action (focus_tracker);
+ if (action != NULL && interface->undo == NULL)
+ gtk_action_set_sensitive (action, FALSE);
+
+ action = e_focus_tracker_get_redo_action (focus_tracker);
+ if (action != NULL && interface->redo == NULL)
+ gtk_action_set_sensitive (action, FALSE);
}
static void
@@ -196,16 +317,22 @@ focus_tracker_targets_received_cb (GtkClipboard *clipboard,
if (focus == NULL)
focus_tracker_disable_actions (focus_tracker);
+ else if (E_IS_SELECTABLE (focus))
+ focus_tracker_selectable_update_actions (
+ focus_tracker, E_SELECTABLE (focus),
+ targets, n_targets);
+
else if (GTK_IS_EDITABLE (focus))
focus_tracker_editable_update_actions (
focus_tracker, GTK_EDITABLE (focus),
targets, n_targets);
- else if (E_IS_SELECTABLE (focus))
- focus_tracker_selectable_update_actions (
- focus_tracker, E_SELECTABLE (focus),
+ else if (GTK_IS_TEXT_VIEW (focus))
+ focus_tracker_text_view_update_actions (
+ focus_tracker, GTK_TEXT_VIEW (focus),
targets, n_targets);
+
g_object_unref (focus_tracker);
}
@@ -215,10 +342,13 @@ focus_tracker_set_focus_cb (GtkWindow *window,
EFocusTracker *focus_tracker)
{
while (focus != NULL) {
+ if (E_IS_SELECTABLE (focus))
+ break;
+
if (GTK_IS_EDITABLE (focus))
break;
- if (E_IS_SELECTABLE (focus))
+ if (GTK_IS_TEXT_VIEW (focus))
break;
focus = gtk_widget_get_parent (focus);
@@ -289,6 +419,18 @@ focus_tracker_set_property (GObject *object,
E_FOCUS_TRACKER (object),
g_value_get_object (value));
return;
+
+ case PROP_UNDO_ACTION:
+ e_focus_tracker_set_undo_action (
+ E_FOCUS_TRACKER (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_REDO_ACTION:
+ e_focus_tracker_set_redo_action (
+ E_FOCUS_TRACKER (object),
+ g_value_get_object (value));
+ return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -349,6 +491,20 @@ focus_tracker_get_property (GObject *object,
e_focus_tracker_get_select_all_action (
E_FOCUS_TRACKER (object)));
return;
+
+ case PROP_UNDO_ACTION:
+ g_value_set_object (
+ value,
+ e_focus_tracker_get_undo_action (
+ E_FOCUS_TRACKER (object)));
+ return;
+
+ case PROP_REDO_ACTION:
+ g_value_set_object (
+ value,
+ e_focus_tracker_get_redo_action (
+ E_FOCUS_TRACKER (object)));
+ return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -534,6 +690,26 @@ e_focus_tracker_class_init (EFocusTrackerClass *class)
NULL,
GTK_TYPE_ACTION,
G_PARAM_READWRITE));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_UNDO_ACTION,
+ g_param_spec_object (
+ "undo-action",
+ "Undo Action",
+ NULL,
+ GTK_TYPE_ACTION,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_REDO_ACTION,
+ g_param_spec_object (
+ "redo-action",
+ "Redo Action",
+ NULL,
+ GTK_TYPE_ACTION,
+ G_PARAM_READWRITE));
}
static void
@@ -787,6 +963,82 @@ e_focus_tracker_set_select_all_action (EFocusTracker *focus_tracker,
g_object_notify (G_OBJECT (focus_tracker), "select-all-action");
}
+GtkAction *
+e_focus_tracker_get_undo_action (EFocusTracker *focus_tracker)
+{
+ g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL);
+
+ return focus_tracker->priv->undo;
+}
+
+void
+e_focus_tracker_set_undo_action (EFocusTracker *focus_tracker,
+ GtkAction *undo)
+{
+ g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker));
+
+ if (undo != NULL) {
+ g_return_if_fail (GTK_IS_ACTION (undo));
+ g_object_ref (undo);
+ }
+
+ if (focus_tracker->priv->undo != NULL) {
+ g_signal_handlers_disconnect_matched (
+ focus_tracker->priv->undo,
+ G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL,
+ focus_tracker);
+ g_object_unref (focus_tracker->priv->undo);
+ }
+
+ focus_tracker->priv->undo = undo;
+
+ if (undo != NULL)
+ g_signal_connect_swapped (
+ undo, "activate",
+ G_CALLBACK (e_focus_tracker_undo),
+ focus_tracker);
+
+ g_object_notify (G_OBJECT (focus_tracker), "undo-action");
+}
+
+GtkAction *
+e_focus_tracker_get_redo_action (EFocusTracker *focus_tracker)
+{
+ g_return_val_if_fail (E_IS_FOCUS_TRACKER (focus_tracker), NULL);
+
+ return focus_tracker->priv->redo;
+}
+
+void
+e_focus_tracker_set_redo_action (EFocusTracker *focus_tracker,
+ GtkAction *redo)
+{
+ g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker));
+
+ if (redo != NULL) {
+ g_return_if_fail (GTK_IS_ACTION (redo));
+ g_object_ref (redo);
+ }
+
+ if (focus_tracker->priv->redo != NULL) {
+ g_signal_handlers_disconnect_matched (
+ focus_tracker->priv->redo,
+ G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL,
+ focus_tracker);
+ g_object_unref (focus_tracker->priv->redo);
+ }
+
+ focus_tracker->priv->redo = redo;
+
+ if (redo != NULL)
+ g_signal_connect_swapped (
+ redo, "activate",
+ G_CALLBACK (e_focus_tracker_redo),
+ focus_tracker);
+
+ g_object_notify (G_OBJECT (focus_tracker), "redo-action");
+}
+
void
e_focus_tracker_update_actions (EFocusTracker *focus_tracker)
{
@@ -813,11 +1065,21 @@ e_focus_tracker_cut_clipboard (EFocusTracker *focus_tracker)
focus = e_focus_tracker_get_focus (focus_tracker);
- if (GTK_IS_EDITABLE (focus))
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_cut_clipboard (E_SELECTABLE (focus));
+
+ else if (GTK_IS_EDITABLE (focus))
gtk_editable_cut_clipboard (GTK_EDITABLE (focus));
- else if (E_IS_SELECTABLE (focus))
- e_selectable_cut_clipboard (E_SELECTABLE (focus));
+ else if (GTK_IS_TEXT_VIEW (focus)) {
+ GtkTextView *text_view = GTK_TEXT_VIEW (focus);
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view);
+ gboolean is_editable = gtk_text_view_get_editable (text_view);
+
+ gtk_text_buffer_cut_clipboard (buffer,
+ gtk_widget_get_clipboard (focus, GDK_SELECTION_CLIPBOARD),
+ is_editable);
+ }
}
void
@@ -829,11 +1091,18 @@ e_focus_tracker_copy_clipboard (EFocusTracker *focus_tracker)
focus = e_focus_tracker_get_focus (focus_tracker);
- if (GTK_IS_EDITABLE (focus))
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_copy_clipboard (E_SELECTABLE (focus));
+
+ else if (GTK_IS_EDITABLE (focus))
gtk_editable_copy_clipboard (GTK_EDITABLE (focus));
- else if (E_IS_SELECTABLE (focus))
- e_selectable_copy_clipboard (E_SELECTABLE (focus));
+ else if (GTK_IS_TEXT_VIEW (focus)) {
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (focus));
+
+ gtk_text_buffer_copy_clipboard (buffer,
+ gtk_widget_get_clipboard (focus, GDK_SELECTION_CLIPBOARD));
+ }
}
void
@@ -845,11 +1114,21 @@ e_focus_tracker_paste_clipboard (EFocusTracker *focus_tracker)
focus = e_focus_tracker_get_focus (focus_tracker);
- if (GTK_IS_EDITABLE (focus))
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_paste_clipboard (E_SELECTABLE (focus));
+
+ else if (GTK_IS_EDITABLE (focus))
gtk_editable_paste_clipboard (GTK_EDITABLE (focus));
- else if (E_IS_SELECTABLE (focus))
- e_selectable_paste_clipboard (E_SELECTABLE (focus));
+ else if (GTK_IS_TEXT_VIEW (focus)) {
+ GtkTextView *text_view = GTK_TEXT_VIEW (focus);
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view);
+ gboolean is_editable = gtk_text_view_get_editable (text_view);
+
+ gtk_text_buffer_paste_clipboard (buffer,
+ gtk_widget_get_clipboard (focus, GDK_SELECTION_CLIPBOARD),
+ NULL, is_editable);
+ }
}
void
@@ -861,11 +1140,19 @@ e_focus_tracker_delete_selection (EFocusTracker *focus_tracker)
focus = e_focus_tracker_get_focus (focus_tracker);
- if (GTK_IS_EDITABLE (focus))
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_delete_selection (E_SELECTABLE (focus));
+
+ else if (GTK_IS_EDITABLE (focus))
gtk_editable_delete_selection (GTK_EDITABLE (focus));
- else if (E_IS_SELECTABLE (focus))
- e_selectable_delete_selection (E_SELECTABLE (focus));
+ else if (GTK_IS_TEXT_VIEW (focus)) {
+ GtkTextView *text_view = GTK_TEXT_VIEW (focus);
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view);
+ gboolean is_editable = gtk_text_view_get_editable (text_view);
+
+ gtk_text_buffer_delete_selection (buffer, TRUE, is_editable);
+ }
}
void
@@ -877,9 +1164,47 @@ e_focus_tracker_select_all (EFocusTracker *focus_tracker)
focus = e_focus_tracker_get_focus (focus_tracker);
- if (GTK_IS_EDITABLE (focus))
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_select_all (E_SELECTABLE (focus));
+
+ else if (GTK_IS_EDITABLE (focus))
gtk_editable_select_region (GTK_EDITABLE (focus), 0, -1);
- else if (E_IS_SELECTABLE (focus))
- e_selectable_select_all (E_SELECTABLE (focus));
+ else if (GTK_IS_TEXT_VIEW (focus)) {
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (focus));
+ GtkTextIter start, end;
+
+ gtk_text_buffer_get_bounds (buffer, &start, &end);
+ gtk_text_buffer_select_range (buffer, &start, &end);
+ }
+}
+
+void
+e_focus_tracker_undo (EFocusTracker *focus_tracker)
+{
+ GtkWidget *focus;
+
+ g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker));
+
+ focus = e_focus_tracker_get_focus (focus_tracker);
+
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_undo (E_SELECTABLE (focus));
+ else
+ e_widget_undo_do_undo (focus);
+}
+
+void
+e_focus_tracker_redo (EFocusTracker *focus_tracker)
+{
+ GtkWidget *focus;
+
+ g_return_if_fail (E_IS_FOCUS_TRACKER (focus_tracker));
+
+ focus = e_focus_tracker_get_focus (focus_tracker);
+
+ if (E_IS_SELECTABLE (focus))
+ e_selectable_redo (E_SELECTABLE (focus));
+ else
+ e_widget_undo_do_redo (focus);
}
diff --git a/e-util/e-focus-tracker.h b/e-util/e-focus-tracker.h
index b837e52997..a4e5a4e210 100644
--- a/e-util/e-focus-tracker.h
+++ b/e-util/e-focus-tracker.h
@@ -90,6 +90,12 @@ GtkAction * e_focus_tracker_get_select_all_action
void e_focus_tracker_set_select_all_action
(EFocusTracker *focus_tracker,
GtkAction *select_all);
+GtkAction * e_focus_tracker_get_undo_action (EFocusTracker *focus_tracker);
+void e_focus_tracker_set_undo_action (EFocusTracker *focus_tracker,
+ GtkAction *undo);
+GtkAction * e_focus_tracker_get_redo_action (EFocusTracker *focus_tracker);
+void e_focus_tracker_set_redo_action (EFocusTracker *focus_tracker,
+ GtkAction *redo);
void e_focus_tracker_update_actions (EFocusTracker *focus_tracker);
void e_focus_tracker_cut_clipboard (EFocusTracker *focus_tracker);
void e_focus_tracker_copy_clipboard (EFocusTracker *focus_tracker);
@@ -97,6 +103,8 @@ void e_focus_tracker_paste_clipboard (EFocusTracker *focus_tracker);
void e_focus_tracker_delete_selection
(EFocusTracker *focus_tracker);
void e_focus_tracker_select_all (EFocusTracker *focus_tracker);
+void e_focus_tracker_undo (EFocusTracker *focus_tracker);
+void e_focus_tracker_redo (EFocusTracker *focus_tracker);
G_END_DECLS
diff --git a/e-util/e-selectable.c b/e-util/e-selectable.c
index d19adb8304..6b011ee33b 100644
--- a/e-util/e-selectable.c
+++ b/e-util/e-selectable.c
@@ -134,6 +134,32 @@ e_selectable_select_all (ESelectable *selectable)
interface->select_all (selectable);
}
+void
+e_selectable_undo (ESelectable *selectable)
+{
+ ESelectableInterface *interface;
+
+ g_return_if_fail (E_IS_SELECTABLE (selectable));
+
+ interface = E_SELECTABLE_GET_INTERFACE (selectable);
+
+ if (interface->undo != NULL)
+ interface->undo (selectable);
+}
+
+void
+e_selectable_redo (ESelectable *selectable)
+{
+ ESelectableInterface *interface;
+
+ g_return_if_fail (E_IS_SELECTABLE (selectable));
+
+ interface = E_SELECTABLE_GET_INTERFACE (selectable);
+
+ if (interface->redo != NULL)
+ interface->redo (selectable);
+}
+
GtkTargetList *
e_selectable_get_copy_target_list (ESelectable *selectable)
{
diff --git a/e-util/e-selectable.h b/e-util/e-selectable.h
index c03cb3da2c..2d92941986 100644
--- a/e-util/e-selectable.h
+++ b/e-util/e-selectable.h
@@ -62,6 +62,8 @@ struct _ESelectableInterface {
void (*paste_clipboard) (ESelectable *selectable);
void (*delete_selection) (ESelectable *selectable);
void (*select_all) (ESelectable *selectable);
+ void (*undo) (ESelectable *selectable);
+ void (*redo) (ESelectable *selectable);
};
GType e_selectable_get_type (void) G_GNUC_CONST;
@@ -74,6 +76,8 @@ void e_selectable_copy_clipboard (ESelectable *selectable);
void e_selectable_paste_clipboard (ESelectable *selectable);
void e_selectable_delete_selection (ESelectable *selectable);
void e_selectable_select_all (ESelectable *selectable);
+void e_selectable_undo (ESelectable *selectable);
+void e_selectable_redo (ESelectable *selectable);
GtkTargetList * e_selectable_get_copy_target_list
(ESelectable *selectable);
GtkTargetList * e_selectable_get_paste_target_list
diff --git a/e-util/e-util.h b/e-util/e-util.h
index aa2521f35a..784858e99f 100644
--- a/e-util/e-util.h
+++ b/e-util/e-util.h
@@ -223,6 +223,7 @@
#include <e-util/e-web-view-gtkhtml.h>
#include <e-util/e-web-view-preview.h>
#include <e-util/e-web-view.h>
+#include <e-util/e-widget-undo.h>
#include <e-util/e-xml-utils.h>
#include <e-util/ea-cell-table.h>
#include <e-util/ea-factory.h>
diff --git a/e-util/e-widget-undo.c b/e-util/e-widget-undo.c
new file mode 100644
index 0000000000..4cb933f544
--- /dev/null
+++ b/e-util/e-widget-undo.c
@@ -0,0 +1,891 @@
+/*
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ *
+ * Authors:
+ * Milan Crha <mcrha@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <gtk/gtk.h>
+#include <glib/gi18n-lib.h>
+
+#include <string.h>
+
+#include "e-focus-tracker.h"
+#include "e-widget-undo.h"
+
+#define DEFAULT_MAX_UNDO_LEVEL 256
+#define UNDO_DATA_KEY "e-undo-data-ptr"
+
+/* calculates real index in EUndoData::undo_stack */
+#define REAL_INDEX(x) ((data->undo_from + (x) + 2 * data->undo_len) % data->undo_len)
+
+typedef enum {
+ E_UNDO_INSERT,
+ E_UNDO_DELETE
+} EUndoType;
+
+typedef enum {
+ E_UNDO_DO_UNDO,
+ E_UNDO_DO_REDO
+} EUndoDoType;
+
+typedef struct _EUndoInfo {
+ EUndoType type;
+ gchar *text;
+ gint position_start;
+ gint position_end; /* valid for delete type only */
+} EUndoInfo;
+
+typedef struct _EUndoData {
+ EUndoInfo **undo_stack; /* stack for undo, with max_undo_level elements, some are NULL */
+ gint undo_len; /* how many undo actions can be saved */
+ gint undo_from; /* where the first undo action begins */
+ gint n_undos; /* how many undo actions are saved;
+ [(undo_from + n_undos) % undo_len] is the next free undo item (or the first redo) */
+ gint n_redos; /* how many redo actions are saved */
+
+ EUndoInfo *current_info; /* the top undo action */
+
+ gulong insert_handler_id;
+ gulong delete_handler_id;
+} EUndoData;
+
+static void
+free_undo_info (gpointer ptr)
+{
+ EUndoInfo *info = ptr;
+
+ if (info) {
+ g_free (info->text);
+ g_free (info);
+ }
+}
+
+static void
+free_undo_data (gpointer ptr)
+{
+ EUndoData *data = ptr;
+
+ if (data) {
+ gint ii;
+
+ for (ii = 0; ii < data->undo_len; ii++) {
+ free_undo_info (data->undo_stack[ii]);
+ }
+ g_free (data);
+ }
+}
+
+static void
+reset_redos (EUndoData *data)
+{
+ gint ii, index;
+
+ for (ii = 0; ii < data->n_redos; ii++) {
+ index = REAL_INDEX (data->n_undos + ii);
+
+ free_undo_info (data->undo_stack[index]);
+ data->undo_stack[index] = NULL;
+ }
+
+ data->n_redos = 0;
+}
+
+static void
+push_undo (EUndoData *data,
+ EUndoInfo *info)
+{
+ gint index;
+
+ reset_redos (data);
+
+ if (data->n_undos == data->undo_len) {
+ data->undo_from = (data->undo_from + 1) % data->undo_len;
+ } else {
+ data->n_undos++;
+ }
+
+ index = REAL_INDEX (data->n_undos - 1);
+ free_undo_info (data->undo_stack[index]);
+ data->undo_stack[index] = info;
+}
+
+static gboolean
+can_merge_insert_undos (EUndoInfo *current_info,
+ const gchar *text,
+ gint text_len,
+ gint position)
+{
+ gint len;
+
+ /* allow only one letter merge */
+ if (!current_info || current_info->type != E_UNDO_INSERT ||
+ !text || text_len <= 0 || text_len > 1)
+ return FALSE;
+
+ if (text[0] == '\r' || text[0] == '\n')
+ return FALSE;
+
+ len = strlen (current_info->text);
+ if (position != current_info->position_start + len)
+ return FALSE;
+
+ if (g_ascii_isspace (text[0])) {
+ if (len <= 0 || !g_ascii_isspace (current_info->text[len - 1]))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+push_insert_undo (GObject *object,
+ const gchar *text,
+ gint text_len,
+ gint position)
+{
+ EUndoData *data;
+ EUndoInfo *info;
+
+ data = g_object_get_data (object, UNDO_DATA_KEY);
+ if (!data) {
+ g_warn_if_reached ();
+ return;
+ }
+
+ /* one letter long text, divide undos on spaces */
+ if (data->current_info &&
+ can_merge_insert_undos (data->current_info, text, text_len, position)) {
+ gchar *new_text;
+
+ new_text = g_strdup_printf ("%s%*s", data->current_info->text, text_len, text);
+ g_free (data->current_info->text);
+ data->current_info->text = new_text;
+
+ return;
+ }
+
+ info = g_new0 (EUndoInfo, 1);
+ info->type = E_UNDO_INSERT;
+ info->text = g_strndup (text, text_len);
+ info->position_start = position;
+
+ push_undo (data, info);
+
+ data->current_info = info;
+}
+
+static void
+push_delete_undo (GObject *object,
+ gchar *text, /* takes ownership of the 'text' */
+ gint position_start,
+ gint position_end)
+{
+ EUndoData *data;
+ EUndoInfo *info;
+
+ data = g_object_get_data (object, UNDO_DATA_KEY);
+ if (!data) {
+ g_warn_if_reached ();
+ return;
+ }
+
+ if (data->current_info && data->current_info->type == E_UNDO_DELETE &&
+ position_end - position_start == 1 && !g_ascii_isspace (*text)) {
+ info = data->current_info;
+
+ if (info->position_start == position_start) {
+ gchar *new_text;
+
+ new_text = g_strconcat (info->text, text, NULL);
+ g_free (info->text);
+ info->text = new_text;
+ g_free (text);
+
+ info->position_end++;
+
+ return;
+ } else if (data->current_info->position_start == position_end) {
+ gchar *new_text;
+
+ new_text = g_strconcat (text, info->text, NULL);
+ g_free (info->text);
+ info->text = new_text;
+ g_free (text);
+
+ info->position_start = position_start;
+
+ return;
+ }
+ }
+
+ info = g_new0 (EUndoInfo, 1);
+ info->type = E_UNDO_DELETE;
+ info->text = text;
+ info->position_start = position_start;
+ info->position_end = position_end;
+
+ push_undo (data, info);
+
+ data->current_info = info;
+}
+
+static void
+editable_undo_insert_text_cb (GtkEditable *editable,
+ gchar *text,
+ gint text_length,
+ gint *position,
+ gpointer user_data)
+{
+ push_insert_undo (G_OBJECT (editable), text, text_length, *position);
+}
+
+static void
+editable_undo_delete_text_cb (GtkEditable *editable,
+ gint start_pos,
+ gint end_pos,
+ gpointer user_data)
+{
+ push_delete_undo (G_OBJECT (editable), gtk_editable_get_chars (editable, start_pos, end_pos), start_pos, end_pos);
+}
+
+static void
+editable_undo_insert_text (GObject *object,
+ const gchar *text,
+ gint position)
+{
+ g_return_if_fail (GTK_IS_EDITABLE (object));
+
+ gtk_editable_insert_text (GTK_EDITABLE (object), text, -1, &position);
+}
+
+static void
+editable_undo_delete_text (GObject *object,
+ gint position_start,
+ gint position_end)
+{
+ g_return_if_fail (GTK_IS_EDITABLE (object));
+
+ gtk_editable_delete_text (GTK_EDITABLE (object), position_start, position_end);
+}
+
+static void
+text_buffer_undo_insert_text_cb (GtkTextBuffer *text_buffer,
+ GtkTextIter *location,
+ gchar *text,
+ gint text_length,
+ gpointer user_data)
+{
+ push_insert_undo (G_OBJECT (text_buffer), text, text_length, gtk_text_iter_get_offset (location));
+}
+
+static void
+text_buffer_undo_delete_range_cb (GtkTextBuffer *text_buffer,
+ GtkTextIter *start,
+ GtkTextIter *end,
+ gpointer user_data)
+{
+ push_delete_undo (G_OBJECT (text_buffer),
+ gtk_text_iter_get_text (start, end),
+ gtk_text_iter_get_offset (start),
+ gtk_text_iter_get_offset (end));
+}
+
+static void
+text_buffer_undo_insert_text (GObject *object,
+ const gchar *text,
+ gint position)
+{
+ GtkTextBuffer *text_buffer;
+ GtkTextIter iter;
+
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (object));
+
+ text_buffer = GTK_TEXT_BUFFER (object);
+
+ gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, position);
+ gtk_text_buffer_insert (text_buffer, &iter, text, -1);
+}
+
+static void
+text_buffer_undo_delete_text (GObject *object,
+ gint position_start,
+ gint position_end)
+{
+ GtkTextBuffer *text_buffer;
+ GtkTextIter start_iter, end_iter;
+
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (object));
+
+ text_buffer = GTK_TEXT_BUFFER (object);
+
+ gtk_text_buffer_get_iter_at_offset (text_buffer, &start_iter, position_start);
+ gtk_text_buffer_get_iter_at_offset (text_buffer, &end_iter, position_end);
+ gtk_text_buffer_delete (text_buffer, &start_iter, &end_iter);
+}
+
+static void
+widget_undo_place_cursor_at (GObject *object,
+ gint char_pos)
+{
+ if (GTK_IS_EDITABLE (object))
+ gtk_editable_set_position (GTK_EDITABLE (object), char_pos);
+ else if (GTK_IS_TEXT_BUFFER (object)) {
+ GtkTextBuffer *buffer;
+ GtkTextIter pos;
+
+ buffer = GTK_TEXT_BUFFER (object);
+
+ gtk_text_buffer_get_iter_at_offset (buffer, &pos, char_pos);
+ gtk_text_buffer_place_cursor (buffer, &pos);
+ }
+}
+
+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))
+{
+ EUndoData *data;
+ EUndoInfo *info = NULL;
+
+ data = g_object_get_data (object, UNDO_DATA_KEY);
+ if (!data)
+ return;
+
+ if (todo == E_UNDO_DO_UNDO && data->n_undos > 0) {
+ info = data->undo_stack[REAL_INDEX (data->n_undos - 1)];
+ data->n_undos--;
+ data->n_redos++;
+ } else if (todo == E_UNDO_DO_REDO && data->n_redos > 0) {
+ info = data->undo_stack[REAL_INDEX (data->n_undos)];
+ data->n_undos++;
+ data->n_redos--;
+ }
+
+ if (!info)
+ return;
+
+ g_signal_handler_block (object, data->insert_handler_id);
+ g_signal_handler_block (object, data->delete_handler_id);
+
+ if (info->type == E_UNDO_INSERT) {
+ if (todo == E_UNDO_DO_UNDO) {
+ delete_func (object, info->position_start, info->position_start + g_utf8_strlen (info->text, -1));
+ widget_undo_place_cursor_at (object, info->position_start);
+ } else {
+ insert_func (object, info->text, info->position_start);
+ widget_undo_place_cursor_at (object, info->position_start + g_utf8_strlen (info->text, -1));
+ }
+ } else if (info->type == E_UNDO_DELETE) {
+ if (todo == E_UNDO_DO_UNDO) {
+ insert_func (object, info->text, info->position_start);
+ widget_undo_place_cursor_at (object, info->position_start + g_utf8_strlen (info->text, -1));
+ } else {
+ delete_func (object, info->position_start, info->position_end);
+ widget_undo_place_cursor_at (object, info->position_start);
+ }
+ }
+
+ data->current_info = NULL;
+
+ g_signal_handler_unblock (object, data->delete_handler_id);
+ g_signal_handler_unblock (object, data->insert_handler_id);
+}
+
+static gchar *
+undo_describe_info (EUndoInfo *info,
+ EUndoDoType undo_type)
+{
+ if (!info)
+ return NULL;
+
+ if (info->type == E_UNDO_INSERT) {
+ if (undo_type == E_UNDO_DO_UNDO)
+ return g_strdup (_("Undo 'Insert text'"));
+ else
+ return g_strdup (_("Redo 'Insert text'"));
+ /* if (strlen (info->text) > 15) {
+ if (undo_type == E_UNDO_DO_UNDO)
+ return g_strdup_printf (_("Undo 'Insert '%.12s...''"), info->text);
+ else
+ return g_strdup_printf (_("Redo 'Insert '%.12s...''"), info->text);
+ }
+
+ if (undo_type == E_UNDO_DO_UNDO)
+ return g_strdup_printf (_("Undo 'Insert '%s''"), info->text);
+ else
+ return g_strdup_printf (_("Redo 'Insert '%s''"), info->text); */
+ } else if (info->type == E_UNDO_DELETE) {
+ if (undo_type == E_UNDO_DO_UNDO)
+ return g_strdup (_("Undo 'Delete text'"));
+ else
+ return g_strdup (_("Redo 'Delete text'"));
+ /* if (strlen (info->text) > 15) {
+ if (undo_type == E_UNDO_DO_UNDO)
+ return g_strdup_printf (_("Undo 'Delete '%.12s...''"), info->text);
+ else
+ return g_strdup_printf (_("Redo 'Delete '%.12s...''"), info->text);
+ }
+
+ if (undo_type == E_UNDO_DO_UNDO)
+ return g_strdup_printf (_("Undo 'Delete '%s''"), info->text);
+ else
+ return g_strdup_printf (_("Redo 'Delete '%s''"), info->text); */
+ }
+
+ return NULL;
+}
+
+static gboolean
+undo_check_undo (GObject *object,
+ gchar **description)
+{
+ EUndoData *data;
+
+ data = g_object_get_data (object, UNDO_DATA_KEY);
+ if (!data)
+ return FALSE;
+
+ if (data->n_undos <= 0)
+ return FALSE;
+
+ if (description)
+ *description = undo_describe_info (data->undo_stack[REAL_INDEX (data->n_undos - 1)], E_UNDO_DO_UNDO);
+
+ return TRUE;
+}
+
+static gboolean
+undo_check_redo (GObject *object,
+ gchar **description)
+{
+ EUndoData *data;
+
+ data = g_object_get_data (object, UNDO_DATA_KEY);
+ if (!data)
+ return FALSE;
+
+ if (data->n_redos <= 0)
+ return FALSE;
+
+ if (description)
+ *description = undo_describe_info (data->undo_stack[REAL_INDEX (data->n_undos)], E_UNDO_DO_REDO);
+
+ return TRUE;
+}
+
+static void
+undo_reset (GObject *object)
+{
+ EUndoData *data;
+
+ data = g_object_get_data (object, UNDO_DATA_KEY);
+ if (!data)
+ return;
+
+ data->n_undos = 0;
+ data->n_redos = 0;
+}
+
+static void
+widget_undo_popup_activate_cb (GObject *menu_item,
+ GtkWidget *widget)
+{
+ EUndoDoType undo_type = GPOINTER_TO_INT (g_object_get_data (menu_item, UNDO_DATA_KEY));
+
+ if (undo_type == E_UNDO_DO_UNDO)
+ e_widget_undo_do_undo (widget);
+ else
+ e_widget_undo_do_redo (widget);
+}
+
+static gboolean
+widget_undo_prepend_popup (GtkWidget *widget,
+ GtkMenuShell *menu,
+ EUndoDoType undo_type,
+ gboolean already_added)
+{
+ gchar *description = NULL;
+ const gchar *icon_name = NULL;
+
+ if (undo_type == E_UNDO_DO_UNDO && e_widget_undo_has_undo (widget)) {
+ description = e_widget_undo_describe_undo (widget);
+ icon_name = "edit-undo";
+ } else if (undo_type == E_UNDO_DO_REDO && e_widget_undo_has_redo (widget)) {
+ description = e_widget_undo_describe_redo (widget);
+ icon_name = "edit-redo";
+ }
+
+ if (description) {
+ GtkWidget *item, *image;
+
+ if (!already_added) {
+ item = gtk_separator_menu_item_new ();
+ gtk_widget_show (item);
+ gtk_menu_shell_prepend (menu, item);
+
+ already_added = TRUE;
+ }
+
+ image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
+ item = gtk_image_menu_item_new_with_label (description);
+ gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
+ gtk_widget_show (item);
+
+ g_object_set_data (G_OBJECT (item), UNDO_DATA_KEY, GINT_TO_POINTER (undo_type));
+ g_signal_connect (item, "activate", G_CALLBACK (widget_undo_popup_activate_cb), widget);
+
+ gtk_menu_shell_prepend (menu, item);
+
+ g_free (description);
+ }
+
+ return already_added;
+}
+
+static void
+widget_undo_populate_popup_cb (GtkWidget *widget,
+ GtkWidget *popup,
+ gpointer user_data)
+{
+ GtkMenuShell *menu;
+ gboolean added = FALSE;
+
+ if (!GTK_IS_MENU (popup))
+ return;
+
+ menu = GTK_MENU_SHELL (popup);
+
+ /* first redo, because prependend, thus undo gets before it */
+ if (e_widget_undo_has_redo (widget))
+ added = widget_undo_prepend_popup (widget, menu, E_UNDO_DO_REDO, added);
+
+ if (e_widget_undo_has_undo (widget))
+ widget_undo_prepend_popup (widget, menu, E_UNDO_DO_UNDO, added);
+}
+
+/**
+ * e_widget_undo_attach:
+ * @widget: a #GtkWidget, where to attach undo functionality
+ * @focus_tracker: an #EFocusTracker, can be %NULL
+ *
+ * The function does nothing, if the widget is not of a supported type
+ * for undo functionality, same as when the undo is already attached.
+ * It is ensured that the actions of the provided @focus_tracker are
+ * updated on change of the @widget.
+ *
+ * See @e_widget_undo_is_attached().
+ *
+ * Since: 3.12
+ **/
+void
+e_widget_undo_attach (GtkWidget *widget,
+ EFocusTracker *focus_tracker)
+{
+ EUndoData *data;
+
+ if (e_widget_undo_is_attached (widget))
+ return;
+
+ if (GTK_IS_EDITABLE (widget)) {
+ data = g_new0 (EUndoData, 1);
+ data->undo_len = DEFAULT_MAX_UNDO_LEVEL;
+ data->undo_stack = g_new0 (EUndoInfo *, data->undo_len);
+
+ g_object_set_data_full (G_OBJECT (widget), UNDO_DATA_KEY, data, free_undo_data);
+
+ data->insert_handler_id = g_signal_connect (widget, "insert-text",
+ G_CALLBACK (editable_undo_insert_text_cb), NULL);
+ data->delete_handler_id = g_signal_connect (widget, "delete-text",
+ G_CALLBACK (editable_undo_delete_text_cb), NULL);
+
+ if (focus_tracker)
+ g_signal_connect_swapped (widget, "changed",
+ G_CALLBACK (e_focus_tracker_update_actions), focus_tracker);
+
+ if (GTK_IS_ENTRY (widget))
+ g_signal_connect (widget, "populate-popup",
+ G_CALLBACK (widget_undo_populate_popup_cb), NULL);
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ data = g_new0 (EUndoData, 1);
+ data->undo_len = DEFAULT_MAX_UNDO_LEVEL;
+ data->undo_stack = g_new0 (EUndoInfo *, data->undo_len);
+
+ g_object_set_data_full (G_OBJECT (text_buffer), UNDO_DATA_KEY, data, free_undo_data);
+
+ data->insert_handler_id = g_signal_connect (text_buffer, "insert-text",
+ G_CALLBACK (text_buffer_undo_insert_text_cb), NULL);
+ data->delete_handler_id = g_signal_connect (text_buffer, "delete-range",
+ G_CALLBACK (text_buffer_undo_delete_range_cb), NULL);
+
+ if (focus_tracker)
+ g_signal_connect_swapped (text_buffer, "changed",
+ G_CALLBACK (e_focus_tracker_update_actions), focus_tracker);
+
+ g_signal_connect (widget, "populate-popup",
+ G_CALLBACK (widget_undo_populate_popup_cb), NULL);
+ }
+}
+
+/**
+ * e_widget_undo_is_attached:
+ * @widget: a #GtkWidget, where to test whether undo functionality is attached.
+ *
+ * Checks whether the given widget has already attached an undo
+ * functionality - it is done with @e_widget_undo_attach().
+ *
+ * Returns: Whether the given @widget has already attached undo functionality.
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_widget_undo_is_attached (GtkWidget *widget)
+{
+ gboolean res = FALSE;
+
+ if (GTK_IS_EDITABLE (widget)) {
+ res = g_object_get_data (G_OBJECT (widget), UNDO_DATA_KEY) != NULL;
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ res = g_object_get_data (G_OBJECT (text_buffer), UNDO_DATA_KEY) != NULL;
+ }
+
+ return res;
+}
+
+/**
+ * e_widget_undo_has_undo:
+ * @widget: a #GtkWidget
+ *
+ * Returns: Whether the given @widget has any undo available.
+ *
+ * See: @e_widget_undo_describe_undo, @e_widget_undo_do_undo
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_widget_undo_has_undo (GtkWidget *widget)
+{
+ if (GTK_IS_EDITABLE (widget)) {
+ return undo_check_undo (G_OBJECT (widget), NULL);
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ return undo_check_undo (G_OBJECT (text_buffer), NULL);
+ }
+
+ return FALSE;
+}
+
+/**
+ * e_widget_undo_has_redo:
+ * @widget: a #GtkWidget
+ *
+ * Returns: Whether the given @widget has any redo available.
+ *
+ * See: @e_widget_undo_describe_redo, @e_widget_undo_do_redo
+ *
+ * Since: 3.12
+ **/
+gboolean
+e_widget_undo_has_redo (GtkWidget *widget)
+{
+ if (GTK_IS_EDITABLE (widget)) {
+ return undo_check_redo (G_OBJECT (widget), NULL);
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ return undo_check_redo (G_OBJECT (text_buffer), NULL);
+ }
+
+ return FALSE;
+}
+
+/**
+ * e_widget_undo_describe_undo:
+ * @widget: a #GtkWidget
+ *
+ * Returns: (transfer full): Description of a top undo action available
+ * for the @widget, %NULL when there is no undo action. Returned pointer,
+ * if not %NULL, should be freed with g_free().
+ *
+ * See: @e_widget_undo_has_undo, @e_widget_undo_do_undo
+ *
+ * Since: 3.12
+ **/
+gchar *
+e_widget_undo_describe_undo (GtkWidget *widget)
+{
+ gchar *res = NULL;
+
+ if (GTK_IS_EDITABLE (widget)) {
+ if (!undo_check_undo (G_OBJECT (widget), &res)) {
+ g_warn_if_fail (res == NULL);
+ }
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ if (!undo_check_undo (G_OBJECT (text_buffer), &res)) {
+ g_warn_if_fail (res == NULL);
+ }
+ }
+
+ return res;
+}
+
+/**
+ * e_widget_undo_describe_redo:
+ * @widget: a #GtkWidget
+ *
+ * Returns: (transfer full): Description of a top redo action available
+ * for the @widget, %NULL when there is no redo action. Returned pointer,
+ * if not %NULL, should be freed with g_free().
+ *
+ * See: @e_widget_undo_has_redo, @e_widget_undo_do_redo
+ *
+ * Since: 3.12
+ **/
+gchar *
+e_widget_undo_describe_redo (GtkWidget *widget)
+{
+ gchar *res = NULL;
+
+ if (GTK_IS_EDITABLE (widget)) {
+ if (!undo_check_redo (G_OBJECT (widget), &res)) {
+ g_warn_if_fail (res == NULL);
+ }
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ if (!undo_check_redo (G_OBJECT (text_buffer), &res)) {
+ g_warn_if_fail (res == NULL);
+ }
+ }
+
+ return res;
+}
+
+/**
+ * e_widget_undo_do_undo:
+ * @widget: a #GtkWidget
+ *
+ * Applies the top undo action on the @widget, which also remembers
+ * a redo action. It does nothing if the widget doesn't have
+ * attached undo functionality (@e_widget_undo_attach()), neither
+ * when there is no undo action available.
+ *
+ * See: @e_widget_undo_attach, @e_widget_undo_has_undo, @e_widget_undo_describe_undo
+ *
+ * Since: 3.12
+ **/
+void
+e_widget_undo_do_undo (GtkWidget *widget)
+{
+ if (GTK_IS_EDITABLE (widget)) {
+ undo_do_something (G_OBJECT (widget),
+ E_UNDO_DO_UNDO,
+ editable_undo_insert_text,
+ editable_undo_delete_text);
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ undo_do_something (G_OBJECT (text_buffer),
+ E_UNDO_DO_UNDO,
+ text_buffer_undo_insert_text,
+ text_buffer_undo_delete_text);
+ }
+}
+
+/**
+ * e_widget_undo_do_redo:
+ * @widget: a #GtkWidget
+ *
+ * Applies the top redo action on the @widget, which also remembers
+ * an undo action. It does nothing if the widget doesn't have
+ * attached undo functionality (@e_widget_undo_attach()), neither
+ * when there is no redo action available.
+ *
+ * See: @e_widget_undo_attach, @e_widget_undo_has_redo, @e_widget_undo_describe_redo
+ *
+ * Since: 3.12
+ **/
+void
+e_widget_undo_do_redo (GtkWidget *widget)
+{
+ if (GTK_IS_EDITABLE (widget)) {
+ undo_do_something (G_OBJECT (widget),
+ E_UNDO_DO_REDO,
+ editable_undo_insert_text,
+ editable_undo_delete_text);
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ undo_do_something (G_OBJECT (text_buffer),
+ E_UNDO_DO_REDO,
+ text_buffer_undo_insert_text,
+ text_buffer_undo_delete_text);
+ }
+}
+
+/**
+ * e_widget_undo_reset:
+ * @widget: a #GtkWidget, on which might be attached undo functionality
+ *
+ * Resets undo and redo stack to empty on a widget with attached
+ * undo functionality. It does nothing, if the widget does not have
+ * the undo functionality attached (see @e_widget_undo_attach()).
+ *
+ * Since: 3.12
+ **/
+void
+e_widget_undo_reset (GtkWidget *widget)
+{
+ if (GTK_IS_EDITABLE (widget)) {
+ undo_reset (G_OBJECT (widget));
+ } else if (GTK_IS_TEXT_VIEW (widget)) {
+ GtkTextBuffer *text_buffer;
+
+ text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+ undo_reset (G_OBJECT (text_buffer));
+ }
+}
diff --git a/e-util/e-widget-undo.h b/e-util/e-widget-undo.h
new file mode 100644
index 0000000000..848359be93
--- /dev/null
+++ b/e-util/e-widget-undo.h
@@ -0,0 +1,48 @@
+/*
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ *
+ * Authors:
+ * Milan Crha <mcrha@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
+ *
+ */
+
+#if !defined (__E_UTIL_H_INSIDE__) && !defined (LIBEUTIL_COMPILATION)
+#error "Only <e-util/e-util.h> should be included directly."
+#endif
+
+#ifndef E_WIDGET_UNDO_H
+#define E_WIDGET_UNDO_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+struct _EFocusTracker;
+
+void e_widget_undo_attach (GtkWidget *widget,
+ struct _EFocusTracker *focus_tracker);
+gboolean e_widget_undo_is_attached (GtkWidget *widget);
+gboolean e_widget_undo_has_undo (GtkWidget *widget);
+gboolean e_widget_undo_has_redo (GtkWidget *widget);
+gchar * e_widget_undo_describe_undo (GtkWidget *widget);
+gchar * e_widget_undo_describe_redo (GtkWidget *widget);
+void e_widget_undo_do_undo (GtkWidget *widget);
+void e_widget_undo_do_redo (GtkWidget *widget);
+void e_widget_undo_reset (GtkWidget *widget);
+
+G_END_DECLS
+
+#endif /* E_WIDGET_UNDO_H */