Using GUndo

Name

Using GUndo library -- General use of GUndo

Autoconf / Automake issues

In order to use GUndo in a standard GNU project with autoconf and automake, you may add in your configure.in file someting like this :

PKG_CHECK_MODGULES(YOUR_PACKAGE_NAME_HERE,[
gundo-2.0
other-packages-to-include-from-pkgconfig
]
AC_SUBST(YOUR_PACKAGE_NAME_HERE_CFLAGS)
AC_SUBST(YOUR_PACKAGE_NAME_HERE_LIBS)

Then edit your source directory Makefile.am and modify it to add $(YOUR_PACKAGE_NAME_HERE_CFLAGS) to the INCLUDES section, and $(YOUR_PACKAGE_NAME_HERE_LIBS) to the your_program_name_LDADD section.

Then run

#autoconf
#automake
#./configure

and your package is ready for GUndo use. Then you just have to write your program sources !

GUndo concepts

A GUndo instance is a list of actions of different types which handles some info which can be undone or redone.

List

The list in GUndo is implemented as a GFifo, a data structure which acts as a "First In First Out" stack, with a possibly limited length and which emits signals when data is inserted, removed, or when the cursor is moved.

A single list can handle actions of many types in many contexts. You may also define several lists which handle actions in different contexts.

Example 1. Many types in one list

In a GTK application, you may choose to record every event on every input widget in a single list (text insertion/deletion in entries, selections in combos, toggles of check boxes...) so undo operations may concern all the widgets, and would cancel all the inputs in all the widgets in reverse order.

Example 2. Several lists

In the same GTK application, you may chose to set up a more complex behaviour, with binding a undo list with a text entry e.g. independently from the global list, to undo only keystrokes in the entry when it is focused, without having to undo every other actions on other widgets. It is harder to set up, and even if I'm sure it is possible, I haven't tried it yet :-)


Action

An action is an operation or a set of operations which is performed, which can be reproducted or cancelled. It can be illustrated by the fact of typing in a character in a Word Processor, or deleting a file in a filemanager. In GUndo, an action is defined by a type and info (see below).

The current action (the one which is pointed by the GFifo's cursor) is always the last which was performed. So when you undo an action, the current action is undone and then the cursor moves backwards, and when you redo an action, the curso moves forwards first, and then, the next action is redone.


Action type

An action type is a definition on "how-to-handle" a given action. With the examples above, an action type for typing a character will define what info to store to remember of this action, how to undo it (return to the previous state), how to redo it (reproduct the it)... and for the file deletion, the action type will define the way of saving the data before the physical file deletion, and the way to restore this data.


Info

The info is, for a given action, the data that undo/redo operation will use. There might be different types of actions and though different types of info depending on the context.

The best solution, when possible, is to store "incremental" info : if a numeric data is modified somewhere, you had better store the offset between the old value and the new one (see the GtkUndo default action for GtkRange e.g.), so it will be easy at a given step to retrieve the value before the action and after, just applying an offset to the current value. In this case, you may only store the numeric value in the info field of the GUndoAction using one of the GLib type conversion macros

But it is not always possible. So when you have no other choice, for a given action, you will need to store the value or the state before the action is performed, and after. So, getting back to the previous state will only consist in resetting the before data

Programming with GUndo

Here is a code example. The case is a structure (with 2 fields), in which the values are often modified, and for which we want to keep a trace of the modifications. You can check the gundo/testundo to see it running, or examine the gundo/testundo.c for complete source code. It is a simple GTK app, because setting the GLib main loop is quite difficult. For a more realistic example, hack in the GtkUndo code or see GtkUndo-example.

#include <gundo.h>

/*First, the structure to watch*/
typedef struct{
  gint num;
  gchar* name;
} Foo;

Foo fred={-10,"fred"};

typedef struct {
  gchar* name_before;
  gchar* name_after;
}name_info;

/*Now, the undo object*/
GUndo *undo;

/*The enum for the action types*/
enum{
   NUM_CHANGED=0,
   NAME_CHANGED,
};

/*The types. We don't write down the forward declarations to save space*/
GUndoActionType types[]={
  {NUM_CHANGED,"num changed",
   NULL,NULL,       //We do not need any registration/unregistration callbacks.
   undo_num_changed,redo_num_changed,
   NULL,NULL,      //No action constructor /destructor as the data is simple.
   num_changed_to_string,
   NULL,NULL, //No extra functions
   &fred,          //The data to pass to the callbacks
   0,              //We will handle an int through GPOINTER_TO_INT...
   0               //No extra data
  },
  {NAME_CHANGED,"name changed",
   NULL,NULL,
   undo_name_changed,redo_name_changed,
   destroy_name_changed,NULL,      //No action constructor as the data is simple.
   name_changed_to_string,
   NULL,NULL,      //No extra functions
   &fred,          //The data to pass to the callbacks
   sizeof(name_info), //We will save the info as a struct name_info.
   0               //No extra data
  }};


/*The function to alter the num field*/
void change_num(gint new_num){
  gint offset=new_num-fred.num;
  fred.num=new_num;
  
  g_undo_add_action(undo,NUM_CHANGED,GINT_TO_POINTER(offset));
}

/*The function to modify the name field*/
void change_name(gchar* new_name){
  /*Take care to duplicate memory not to get pointers to nothing!*/
  name_info info={g_strdup(fred.name),g_strdup(new_name)};
  
  fred.name=info.name_after;

  g_undo_add_action(undo,NAME_CHANGED,&info);
  /*No trouble with the localy defined "info" as the data is duplicated when type->structsize>0*/
}


/**************************
 * Action types callbacks *
 **************************/
const gchar *
num_changed_to_string(GUndoAction *action,gpointer data)
{
  return g_strdup_printf(_("num changed offset: %d"),GPOINTER_TO_INT(action->info));
}

void
undo_num_changed(GUndoAction *action, gpointer data)
{
  /*Even if fred is global, we can get it through data*/
  Foo *freddy=data;
  /*Now we apply the offset to the num field*/
  freddy->num-=GPOINTER_TO_INT(action->info);
}

void
redo_num_changed(GUndoAction *action, gpointer data)
{
  /*Even if fred is global, we can get it through data*/
  Foo *freddy=data;
  /*Now we apply the offset to the num field*/
  freddy->num+=GPOINTER_TO_INT(action->info);
}

const gchar *
name_changed_to_string(GUndoAction *action,gpointer data)
{
  name_info *info=action->info;
  return g_strdup_printf(_("name changed before: %s after: %s"),info->name_before,info->name_after);
}

void
undo_name_changed(GUndoAction *action, gpointer data)
{
  /*Even if fred is global, we can get it through data*/
  Foo *freddy=data;
  name_info *info=action->info;
  freddy->name=info->name_before;
}

void
redo_name_changed(GUndoAction *action, gpointer data)
{
  /*Even if fred is global, we can get it through data*/
  Foo *freddy=data;
  name_info *info=action->info;
  freddy->name=info->name_after;
}

void
destroy_name_changed(GUndoAction *action)
{
  name_info *info=action->info;
  g_free(info->name_before);
  g_free(info->name_after);
}


/******************
 * Undo callbacks *
 ******************/
gboolean undo_at_start=TRUE;
gboolean undo_at_end=TRUE;


on_undo(GUndo *undo,GUndoAction *action,gboolean at_beginning,gpointer data){
  undo_at_end=FALSE;
  undo_at_start=at_beginning;
}

on_redo(GUndo *undo,GUndoAction *action,gboolean at_end,gpointer data){
  undo_at_end=at_end;
  undo_at_start=FALSE;
}

on_add_action(GUndo *undo,GUndoAction *action,gpointer data){
  undo_at_start=FALSE;
}

/*****************
 * Main function *
 *****************/


int 
main (int argc,char **argv){
  ... GTK initialisation ...

  undo=g_undo_new(types); //We instantiate a new GUndo, with default max length (10) and the two types registered.
  
  g_signal_connect(G_OBJECT(undo),"undo",G_CALLBACK(on_undo),NULL);
  g_signal_connect(G_OBJECT(undo),"redo",G_CALLBACK(on_redo),NULL);

  ... Interface creation & main loop ...
}