909 lines
23 KiB
C
909 lines
23 KiB
C
/*
|
|
* 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));
|
|
}
|
|
}
|