mirror of https://github.com/GNOME/gimp.git
928 lines
27 KiB
C
928 lines
27 KiB
C
/* The GIMP -- an image manipulation program
|
|
* Copyright (C) 1995-1999 Spencer Kimball and Peter Mattis
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 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 General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
*
|
|
* Undo history browser by Austin Donnelly <austin@gimp.org>
|
|
*/
|
|
|
|
|
|
/* TODO:
|
|
*
|
|
* - reuse the L&C previews?
|
|
* Currently we use gimp_image_construct_composite_preview ()
|
|
* which makes use of the preview_cache on a per layer basis.
|
|
*
|
|
* - work out which (if any) is the clean image, and mark it as such.
|
|
* Currently, it's on the wrong line.
|
|
*
|
|
* - undo names are less than useful. This isn't a problem with
|
|
* undo_history.c itself, more with the rather chaotic way
|
|
* people have of picking an undo type when pushing undos, and
|
|
* inconsistent use of undo groups. Maybe rather than
|
|
* specifying an (enum) type, it should be a const char * ?
|
|
*
|
|
* BUGS:
|
|
* - clean pixmap in wrong place
|
|
*
|
|
* Initial rev 0.01, (c) 19 Sept 1999 Austin Donnelly <austin@gimp.org>
|
|
*
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "libgimpbase/gimpbase.h"
|
|
#include "libgimpwidgets/gimpwidgets.h"
|
|
|
|
#include "core/core-types.h"
|
|
|
|
#include "base/pixel-region.h"
|
|
#include "base/temp-buf.h"
|
|
|
|
#include "paint-funcs/paint-funcs.h"
|
|
|
|
#include "core/gimpdrawable.h"
|
|
#include "core/gimpimage.h"
|
|
#include "core/gimpimage-mask.h"
|
|
|
|
#include "gimprc.h"
|
|
#include "undo.h"
|
|
|
|
#include "libgimp/gimpintl.h"
|
|
|
|
#include "pixmaps/raise.xpm"
|
|
#include "pixmaps/lower.xpm"
|
|
#include "pixmaps/yes.xpm"
|
|
#include "pixmaps/question.xpm"
|
|
|
|
|
|
typedef struct
|
|
{
|
|
GimpImage *gimage; /* image we're tracking undo info for */
|
|
GtkWidget *shell; /* dialog window */
|
|
GtkWidget *clist; /* list of undo actions */
|
|
GtkWidget *undo_button; /* button to undo an operation */
|
|
GtkWidget *redo_button; /* button to redo an operation */
|
|
int old_selection; /* previous selection in the clist */
|
|
int preview_size; /* size of the previews (from preferences) */
|
|
} undo_history_st;
|
|
|
|
typedef struct
|
|
{
|
|
GtkCList *clist;
|
|
gint row;
|
|
gint size;
|
|
GimpImage *gimage;
|
|
} idle_preview_args;
|
|
|
|
/*
|
|
* Theory of operation.
|
|
*
|
|
* Keep a clist. Each row of the clist corresponds to an image as it
|
|
* was at some time in the past, present or future. The selected row
|
|
* is the present image. Rows below the selected one are in the
|
|
* future - as redo operations are performed, they become the current
|
|
* image. Rows above the selected one are in the past - undo
|
|
* operations move the highlight up.
|
|
*
|
|
* The slight fly in the ointment is that if rows are images, then how
|
|
* should they be labelled? An undo or redo operation goes _between_
|
|
* two image states - it isn't an image state. It's a pretty
|
|
* arbitrary decision, but I've chosen to label a row with the name of
|
|
* the action that brought the image into the state represented by
|
|
* that row. Thus, there is a special first row without a meaningful
|
|
* label, which represents the image state before the first action has
|
|
* been done to it. The choice is between a special first row or a
|
|
* special last row. Since people mostly work near the leading edge,
|
|
* not often going all the way back, I've chosen to put the special
|
|
* case out of common sight.
|
|
*
|
|
* So, the undo stack contents appear above the selected row, and the
|
|
* redo stack below it.
|
|
*
|
|
* The clist is initialised by mapping over the undo and redo stack.
|
|
*
|
|
* Once initialised, the dialog listens to undo_event signals from the
|
|
* gimage. These undo events allow us to track changes to the undo
|
|
* and redo stacks. We follow the events, making parallel changes to
|
|
* the clist. If we ever get out of sync, there is no mechanism to
|
|
* notice or re-sync. A few g_return_if_fails should catch some of
|
|
* these cases.
|
|
*
|
|
* User clicks changing the selected row in the clist turn into
|
|
* multiple calls to undo_pop or undo_redo, with appropriate signals
|
|
* blocked so we don't get our own events back.
|
|
*
|
|
* The "Close" button hides the dialog, rather than destroying it.
|
|
* This may well need to be changed, since the dialog will continue to
|
|
* track updates, and if it's generating previews this might take too
|
|
* long for large images.
|
|
*
|
|
* The dialog is destroyed when the gimage it is tracking is
|
|
* destroyed. Note that a File/Revert destroys the current gimage and
|
|
* so blows the undo/redo stacks.
|
|
*
|
|
* --austin, 19/9/1999
|
|
*/
|
|
|
|
/**************************************************************/
|
|
/* Static Data */
|
|
|
|
static GdkPixmap *clean_pixmap = NULL;
|
|
static GdkBitmap *clean_mask = NULL;
|
|
|
|
static GdkPixmap *clear_pixmap = NULL;
|
|
static GdkBitmap *clear_mask = NULL;
|
|
|
|
|
|
static void undo_history_undo_event (GtkWidget *widget,
|
|
gint ev,
|
|
gpointer data);
|
|
static void undo_history_clean_callback (GtkWidget *widget,
|
|
gpointer data);
|
|
|
|
static void undo_history_select_row_callback (GtkWidget *widget,
|
|
gint row,
|
|
gint column,
|
|
gpointer event,
|
|
gpointer data);
|
|
|
|
|
|
/**************************************************************/
|
|
/* Local functions */
|
|
|
|
|
|
static MaskBuf *
|
|
mask_render_preview (GimpImage *gimage,
|
|
gint *pwidth,
|
|
gint *pheight)
|
|
{
|
|
GimpChannel *mask;
|
|
MaskBuf *scaled_buf = NULL;
|
|
PixelRegion srcPR, destPR;
|
|
gint subsample;
|
|
gint width, height;
|
|
gint scale;
|
|
|
|
mask = gimp_image_get_mask (gimage);
|
|
if ((gimp_drawable_width (GIMP_DRAWABLE(mask)) > *pwidth) ||
|
|
(gimp_drawable_height (GIMP_DRAWABLE(mask)) > *pheight))
|
|
{
|
|
if (((gfloat) gimp_drawable_width (GIMP_DRAWABLE (mask)) / (gfloat) *pwidth) >
|
|
((gfloat) gimp_drawable_height (GIMP_DRAWABLE (mask)) / (gfloat) *pheight))
|
|
{
|
|
width = *pwidth;
|
|
height = (gimp_drawable_height (GIMP_DRAWABLE (mask)) * (*pwidth)) / gimp_drawable_width (GIMP_DRAWABLE (mask));
|
|
}
|
|
else
|
|
{
|
|
width = (gimp_drawable_width (GIMP_DRAWABLE (mask)) * (*pheight)) / gimp_drawable_height (GIMP_DRAWABLE (mask));
|
|
height = *pheight;
|
|
}
|
|
|
|
scale = TRUE;
|
|
}
|
|
else
|
|
{
|
|
width = gimp_drawable_width (GIMP_DRAWABLE (mask));
|
|
height = gimp_drawable_height (GIMP_DRAWABLE (mask));
|
|
|
|
scale = FALSE;
|
|
}
|
|
|
|
/* if the mask is empty, no need to scale and update again */
|
|
if (gimage_mask_is_empty (gimage))
|
|
return NULL;
|
|
|
|
if (scale)
|
|
{
|
|
/* calculate 'acceptable' subsample */
|
|
subsample = 1;
|
|
while ((width * (subsample + 1) * 2 < gimp_drawable_width (GIMP_DRAWABLE (mask))) &&
|
|
(height * (subsample + 1) * 2 < gimp_drawable_height (GIMP_DRAWABLE (mask))))
|
|
subsample = subsample + 1;
|
|
|
|
pixel_region_init (&srcPR, gimp_drawable_data (GIMP_DRAWABLE (mask)),
|
|
0, 0,
|
|
gimp_drawable_width (GIMP_DRAWABLE (mask)),
|
|
gimp_drawable_height (GIMP_DRAWABLE (mask)), FALSE);
|
|
|
|
scaled_buf = mask_buf_new (width, height);
|
|
destPR.bytes = 1;
|
|
destPR.x = 0;
|
|
destPR.y = 0;
|
|
destPR.w = width;
|
|
destPR.h = height;
|
|
destPR.rowstride = srcPR.bytes * width;
|
|
destPR.data = mask_buf_data (scaled_buf);
|
|
destPR.tiles = NULL;
|
|
|
|
subsample_region (&srcPR, &destPR, subsample);
|
|
}
|
|
else
|
|
{
|
|
pixel_region_init (&srcPR, gimp_drawable_data (GIMP_DRAWABLE (mask)),
|
|
0, 0,
|
|
gimp_drawable_width (GIMP_DRAWABLE (mask)),
|
|
gimp_drawable_height (GIMP_DRAWABLE (mask)), FALSE);
|
|
|
|
scaled_buf = mask_buf_new (width, height);
|
|
destPR.bytes = 1;
|
|
destPR.x = 0;
|
|
destPR.y = 0;
|
|
destPR.w = width;
|
|
destPR.h = height;
|
|
destPR.rowstride = srcPR.bytes * width;
|
|
destPR.data = mask_buf_data (scaled_buf);
|
|
destPR.tiles = NULL;
|
|
|
|
copy_region (&srcPR, &destPR);
|
|
}
|
|
|
|
*pheight = height;
|
|
*pwidth = width;
|
|
return scaled_buf;
|
|
}
|
|
|
|
|
|
static gint
|
|
undo_history_set_pixmap_idle (gpointer data)
|
|
{
|
|
idle_preview_args *idle = data;
|
|
static GdkGC *gc = NULL;
|
|
TempBuf *buf = NULL;
|
|
GdkPixmap *pixmap;
|
|
UndoType utype;
|
|
MaskBuf *mbuf = NULL;
|
|
guchar *src;
|
|
gdouble r, g, b, a;
|
|
gdouble c0, c1;
|
|
guchar *p0, *p1, *even, *odd;
|
|
gint width, height, bpp;
|
|
gint x, y;
|
|
|
|
if (!gc)
|
|
gc = gdk_gc_new (GTK_WIDGET (idle->clist)->window);
|
|
|
|
width = idle->gimage->width;
|
|
height = idle->gimage->height;
|
|
|
|
/* Get right aspect ratio */
|
|
if (width > height)
|
|
{
|
|
height = (gint)(((gdouble)idle->size * (gdouble)height) / (gdouble)width + 0.5);
|
|
width = (gint)(((gdouble)width * (gdouble)height)/ (gdouble)idle->gimage->height + 0.5);
|
|
}
|
|
else
|
|
{
|
|
width = (gint)(((gdouble)idle->size * (gdouble)width) / (gdouble)height + 0.5);
|
|
height = (gint)(((gdouble)height * (gdouble)width ) /(gdouble) idle->gimage->width + 0.5);
|
|
}
|
|
|
|
utype = undo_get_undo_top_type (idle->gimage);
|
|
|
|
if ((utype != MASK_UNDO && utype != QMASK_UNDO) ||
|
|
(mbuf = mask_render_preview (idle->gimage, &width, &height)) == NULL)
|
|
{
|
|
buf = gimp_viewable_get_new_preview (GIMP_VIEWABLE (idle->gimage),
|
|
width,
|
|
height);
|
|
bpp = buf->bytes;
|
|
src = temp_buf_data (buf);
|
|
}
|
|
else
|
|
{
|
|
src = mask_buf_data (mbuf);
|
|
bpp = 1; /* Always the case for masks */
|
|
}
|
|
|
|
pixmap = gdk_pixmap_new (GTK_WIDGET (idle->clist)->window,
|
|
width + 2, height + 2,
|
|
-1);
|
|
|
|
gdk_draw_rectangle (pixmap,
|
|
GTK_WIDGET (idle->clist)->style->black_gc,
|
|
TRUE,
|
|
0, 0,
|
|
width + 2, height + 2);
|
|
|
|
even = g_malloc (width * 3);
|
|
odd = g_malloc (width * 3);
|
|
|
|
for (y = 0; y < height; y++)
|
|
{
|
|
p0 = even;
|
|
p1 = odd;
|
|
|
|
for (x = 0; x < width; x++)
|
|
{
|
|
if (bpp == 4)
|
|
{
|
|
r = ((gdouble) src[x*4+0]) / 255.0;
|
|
g = ((gdouble) src[x*4+1]) / 255.0;
|
|
b = ((gdouble) src[x*4+2]) / 255.0;
|
|
a = ((gdouble) src[x*4+3]) / 255.0;
|
|
}
|
|
else if (bpp == 3)
|
|
{
|
|
r = ((gdouble) src[x*3+0]) / 255.0;
|
|
g = ((gdouble) src[x*3+1]) / 255.0;
|
|
b = ((gdouble) src[x*3+2]) / 255.0;
|
|
a = 1.0;
|
|
}
|
|
else
|
|
{
|
|
r = ((gdouble) src[x*bpp+0]) / 255.0;
|
|
g = b = r;
|
|
if (bpp == 2)
|
|
a = ((gdouble) src[x*bpp+1]) / 255.0;
|
|
else
|
|
a = 1.0;
|
|
}
|
|
|
|
if ((x / GIMP_CHECK_SIZE_SM) & 1)
|
|
{
|
|
c0 = GIMP_CHECK_LIGHT;
|
|
c1 = GIMP_CHECK_DARK;
|
|
}
|
|
else
|
|
{
|
|
c0 = GIMP_CHECK_DARK;
|
|
c1 = GIMP_CHECK_LIGHT;
|
|
}
|
|
|
|
*p0++ = (c0 + (r - c0) * a) * 255.0;
|
|
*p0++ = (c0 + (g - c0) * a) * 255.0;
|
|
*p0++ = (c0 + (b - c0) * a) * 255.0;
|
|
|
|
*p1++ = (c1 + (r - c1) * a) * 255.0;
|
|
*p1++ = (c1 + (g - c1) * a) * 255.0;
|
|
*p1++ = (c1 + (b - c1) * a) * 255.0;
|
|
|
|
}
|
|
|
|
if ((y / GIMP_CHECK_SIZE_SM) & 1)
|
|
{
|
|
gdk_draw_rgb_image (pixmap, gc,
|
|
1, y + 1,
|
|
width, 1,
|
|
GDK_RGB_DITHER_NORMAL,
|
|
(guchar *) odd, 3);
|
|
}
|
|
else
|
|
{
|
|
gdk_draw_rgb_image (pixmap, gc,
|
|
1, y + 1,
|
|
width, 1,
|
|
GDK_RGB_DITHER_NORMAL,
|
|
(guchar *) even, 3);
|
|
}
|
|
src += width * bpp;
|
|
}
|
|
|
|
g_free (even);
|
|
g_free (odd);
|
|
|
|
if (buf)
|
|
temp_buf_free (buf);
|
|
if (mbuf)
|
|
mask_buf_free (mbuf);
|
|
|
|
gtk_clist_set_row_data (idle->clist, idle->row, (gpointer)2);
|
|
gtk_clist_set_pixmap (idle->clist, idle->row, 0, pixmap, NULL);
|
|
gdk_drawable_unref (pixmap);
|
|
|
|
return (FALSE);
|
|
}
|
|
|
|
/* check if a preview is already made, otherwise gtk_idle_add the pixmap func */
|
|
static void
|
|
undo_history_set_pixmap (GtkCList *clist,
|
|
gint row,
|
|
gint size,
|
|
GimpImage *gimage)
|
|
{
|
|
static idle_preview_args idle;
|
|
|
|
if (!size || ((gint)gtk_clist_get_row_data (clist, row)) == 2)
|
|
return;
|
|
|
|
idle.clist = clist;
|
|
idle.row = row;
|
|
idle.size = size;
|
|
idle.gimage = gimage;
|
|
|
|
gtk_idle_add ((GtkFunction)undo_history_set_pixmap_idle, &idle);
|
|
}
|
|
|
|
|
|
/* close button clicked */
|
|
static void
|
|
undo_history_close_callback (GtkWidget *widget,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
gtk_widget_hide (GTK_WIDGET (st->shell));
|
|
}
|
|
|
|
/* The gimage and shell destroy callbacks are split so we can:
|
|
* a) blow the shell when the image dissappears
|
|
* b) disconnect from the image if the shell dissappears (we don't
|
|
* want signals from the image to carry on using "st" once it's
|
|
* been freed.
|
|
*/
|
|
|
|
/* gimage renamed */
|
|
static void
|
|
undo_history_gimage_rename_callback (GimpImage *gimage,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
gchar *basename;
|
|
gchar *title;
|
|
|
|
basename = g_path_get_basename (gimp_image_filename (gimage));
|
|
|
|
title = g_strdup_printf (_("Undo History: %s"), basename);
|
|
|
|
g_free (basename);
|
|
|
|
gtk_window_set_title (GTK_WINDOW (st->shell), title);
|
|
|
|
g_free (title);
|
|
}
|
|
|
|
/* gimage destroyed */
|
|
static void
|
|
undo_history_gimage_destroy_callback (GimpImage *gimage,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
|
|
st->gimage = NULL; /* not allowed to use this any more */
|
|
|
|
gtk_widget_destroy (GTK_WIDGET (st->shell));
|
|
/* which continues in the function below: */
|
|
}
|
|
|
|
static void
|
|
undo_history_shell_destroy_callback (GtkWidget *widget,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
|
|
if (st->gimage)
|
|
{
|
|
g_signal_handlers_disconnect_by_func (G_OBJECT (st->gimage),
|
|
undo_history_undo_event,
|
|
st);
|
|
g_signal_handlers_disconnect_by_func (G_OBJECT (st->gimage),
|
|
undo_history_gimage_rename_callback,
|
|
st);
|
|
g_signal_handlers_disconnect_by_func (G_OBJECT (st->gimage),
|
|
undo_history_gimage_destroy_callback,
|
|
st);
|
|
|
|
g_signal_handlers_disconnect_by_func (G_OBJECT (st->gimage),
|
|
undo_history_clean_callback,
|
|
st);
|
|
}
|
|
|
|
g_free (st);
|
|
}
|
|
|
|
/* undo button clicked */
|
|
static void
|
|
undo_history_undo_callback (GtkWidget *widget,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
|
|
undo_pop (st->gimage);
|
|
}
|
|
|
|
/* redo button clicked */
|
|
static void
|
|
undo_history_redo_callback (GtkWidget *widget,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
|
|
undo_redo (st->gimage);
|
|
}
|
|
|
|
|
|
/* Always start clist with dummy entry for image state before
|
|
* the first action on the undo stack */
|
|
static void
|
|
undo_history_prepend_special (GtkCList *clist)
|
|
{
|
|
gchar *name = _("[ base image ]");
|
|
gchar *namelist[3];
|
|
gint row;
|
|
|
|
namelist[0] = NULL;
|
|
namelist[1] = NULL;
|
|
namelist[2] = name;
|
|
|
|
row = gtk_clist_prepend (clist, namelist);
|
|
}
|
|
|
|
|
|
/* Recalculate which of the undo and redo buttons are meant to be sensitive */
|
|
static void
|
|
undo_history_set_sensitive (undo_history_st *st,
|
|
gint rows)
|
|
{
|
|
gtk_widget_set_sensitive (st->undo_button, (st->old_selection != 0));
|
|
gtk_widget_set_sensitive (st->redo_button, (st->old_selection != rows-1));
|
|
}
|
|
|
|
|
|
/* Track undo_event signals, telling us of changes to the undo and
|
|
* redo stacks. */
|
|
static void
|
|
undo_history_undo_event (GtkWidget *widget,
|
|
gint ev,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
undo_event_t event = ev;
|
|
const gchar *name;
|
|
gchar *namelist[3];
|
|
GList *list;
|
|
gint cur_selection;
|
|
GtkCList *clist;
|
|
gint row;
|
|
GdkPixmap *pixmap;
|
|
GdkBitmap *mask;
|
|
|
|
list = GTK_CLIST (st->clist)->selection;
|
|
g_return_if_fail (list != NULL);
|
|
cur_selection = GPOINTER_TO_INT (list->data);
|
|
|
|
clist = GTK_CLIST (st->clist);
|
|
|
|
/* block select events */
|
|
g_signal_handlers_block_by_func (G_OBJECT (st->clist),
|
|
undo_history_select_row_callback,
|
|
st);
|
|
|
|
switch (event)
|
|
{
|
|
case UNDO_PUSHED:
|
|
/* clip everything after the current selection (ie, the
|
|
* actions that are from the redo stack) */
|
|
gtk_clist_freeze (clist);
|
|
while (clist->rows > cur_selection + 1)
|
|
gtk_clist_remove (clist, cur_selection + 1);
|
|
|
|
/* find out what's new */
|
|
name = undo_get_undo_name (st->gimage);
|
|
namelist[0] = NULL;
|
|
namelist[1] = NULL;
|
|
namelist[2] = (char *) name;
|
|
row = gtk_clist_append (clist, namelist);
|
|
g_assert (clist->rows == cur_selection + 2);
|
|
|
|
undo_history_set_pixmap (clist, row, st->preview_size, st->gimage);
|
|
|
|
/* always force selection to bottom, and scroll to it */
|
|
gtk_clist_select_row (clist, clist->rows - 1, -1);
|
|
gtk_clist_thaw (clist);
|
|
gtk_clist_moveto (clist, clist->rows - 1, 0, 1.0, 0.0);
|
|
cur_selection = clist->rows - 1;
|
|
break;
|
|
|
|
case UNDO_EXPIRED:
|
|
/* remove earliest row, but not our special first one */
|
|
if (gtk_clist_get_pixmap (clist, 1, 0, &pixmap, &mask))
|
|
gtk_clist_set_pixmap (clist, 0, 0, pixmap, mask);
|
|
gtk_clist_remove (clist, 1);
|
|
break;
|
|
|
|
case UNDO_POPPED:
|
|
/* move hilight up one */
|
|
g_return_if_fail (cur_selection >= 1);
|
|
gtk_clist_select_row (clist, cur_selection - 1, -1);
|
|
cur_selection--;
|
|
undo_history_set_pixmap (clist, cur_selection, st->preview_size, st->gimage);
|
|
if ( !(gtk_clist_row_is_visible (clist, cur_selection) & GTK_VISIBILITY_FULL))
|
|
gtk_clist_moveto (clist, cur_selection, 0, 0.0, 0.0);
|
|
break;
|
|
|
|
case UNDO_REDO:
|
|
/* move hilight down one */
|
|
g_return_if_fail (cur_selection+1 < clist->rows);
|
|
gtk_clist_select_row (clist, cur_selection+1, -1);
|
|
cur_selection++;
|
|
undo_history_set_pixmap (clist, cur_selection, st->preview_size, st->gimage);
|
|
if ( !(gtk_clist_row_is_visible (clist, cur_selection) & GTK_VISIBILITY_FULL))
|
|
gtk_clist_moveto (clist, cur_selection, 0, 1.0, 0.0);
|
|
break;
|
|
|
|
case UNDO_FREE:
|
|
/* clear all info other that the special first line */
|
|
gtk_clist_freeze (clist);
|
|
gtk_clist_clear (clist);
|
|
undo_history_prepend_special (clist);
|
|
gtk_clist_thaw (clist);
|
|
cur_selection = 0;
|
|
break;
|
|
}
|
|
|
|
/* if the image is clean, set the clean pixmap */
|
|
if (st->gimage->dirty == 0)
|
|
gtk_clist_set_pixmap (clist, cur_selection, 1, clean_pixmap, clean_mask);
|
|
|
|
g_signal_handlers_unblock_by_func (G_OBJECT (st->clist),
|
|
undo_history_select_row_callback,
|
|
st);
|
|
|
|
st->old_selection = cur_selection;
|
|
undo_history_set_sensitive (st, clist->rows);
|
|
}
|
|
|
|
|
|
static void
|
|
undo_history_select_row_callback (GtkWidget *widget,
|
|
gint row,
|
|
gint column,
|
|
gpointer event,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
gint cur_selection;
|
|
|
|
cur_selection = row;
|
|
|
|
if (cur_selection == st->old_selection)
|
|
return;
|
|
|
|
/* Disable undo_event signals while we do these multiple undo or
|
|
* redo actions. */
|
|
g_signal_handlers_block_by_func (G_OBJECT (st->gimage),
|
|
undo_history_undo_event, st);
|
|
|
|
while (cur_selection < st->old_selection)
|
|
{
|
|
undo_pop (st->gimage);
|
|
st->old_selection--;
|
|
}
|
|
while (cur_selection > st->old_selection)
|
|
{
|
|
undo_redo (st->gimage);
|
|
st->old_selection++;
|
|
}
|
|
|
|
undo_history_set_pixmap (GTK_CLIST (widget), cur_selection, st->preview_size, st->gimage);
|
|
|
|
/* if the image is clean, set the clean pixmap */
|
|
if (st->gimage->dirty == 0)
|
|
gtk_clist_set_pixmap (GTK_CLIST (widget), cur_selection, 1, clean_pixmap, clean_mask);
|
|
|
|
g_signal_handlers_unblock_by_func (G_OBJECT (st->gimage),
|
|
undo_history_undo_event, st);
|
|
|
|
undo_history_set_sensitive (st, GTK_CLIST(st->clist)->rows);
|
|
}
|
|
|
|
|
|
static void
|
|
undo_history_clean_callback (GtkWidget *widget,
|
|
gpointer data)
|
|
{
|
|
undo_history_st *st = data;
|
|
gint i;
|
|
gint nrows;
|
|
GtkCList *clist;
|
|
|
|
if (st->gimage->dirty != 0)
|
|
return;
|
|
|
|
/*
|
|
* The image has become clean. Remove the clean_pixmap from
|
|
* all entries. It will be set in the undo_event or select_row
|
|
* callbacks.
|
|
* Ugly, but works better than before. The actual problem is
|
|
* that the "clean" signal is emitted before UNDO_POPPED event,
|
|
* so we can not simply set the clean pixmap here.
|
|
*/
|
|
|
|
clist = GTK_CLIST (st->clist);
|
|
nrows = clist->rows;
|
|
|
|
gtk_clist_freeze (clist);
|
|
for (i=0; i < nrows; i++)
|
|
gtk_clist_set_text (clist, i, 1, NULL);
|
|
gtk_clist_thaw (clist);
|
|
}
|
|
|
|
|
|
/* Used to build up initial contents of clist */
|
|
static gboolean
|
|
undo_history_init_undo (const gchar *undoitemname,
|
|
void *data)
|
|
{
|
|
undo_history_st *st = data;
|
|
gchar *namelist[3];
|
|
gint row;
|
|
|
|
namelist[0] = NULL;
|
|
namelist[1] = NULL;
|
|
namelist[2] = (gchar *) undoitemname;
|
|
row = gtk_clist_prepend (GTK_CLIST (st->clist), namelist);
|
|
gtk_clist_set_pixmap (GTK_CLIST (st->clist), row, 0,
|
|
clear_pixmap, clear_mask);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/* Ditto */
|
|
static gboolean
|
|
undo_history_init_redo (const char *undoitemname,
|
|
void *data)
|
|
{
|
|
undo_history_st *st = data;
|
|
gchar *namelist[3];
|
|
gint row;
|
|
|
|
namelist[0] = NULL; namelist[1] = NULL;
|
|
namelist[2] = (gchar *) undoitemname;
|
|
row = gtk_clist_append (GTK_CLIST (st->clist), namelist);
|
|
gtk_clist_set_pixmap (GTK_CLIST (st->clist), row, 0,
|
|
clear_pixmap, clear_mask);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/*************************************************************/
|
|
/* Publicly exported function */
|
|
|
|
GtkWidget *
|
|
undo_history_new (GimpImage *gimage)
|
|
{
|
|
undo_history_st *st;
|
|
GtkWidget *vbox;
|
|
GtkWidget *hbox;
|
|
GtkWidget *button;
|
|
GtkWidget *scrolled_win;
|
|
|
|
st = g_new0 (undo_history_st, 1);
|
|
st->gimage = gimage;
|
|
st->preview_size = gimprc.preview_size;
|
|
|
|
/* gimage signals */
|
|
g_signal_connect (G_OBJECT (gimage), "undo_event",
|
|
G_CALLBACK (undo_history_undo_event),
|
|
st);
|
|
g_signal_connect (G_OBJECT (gimage), "name_changed",
|
|
G_CALLBACK (undo_history_gimage_rename_callback),
|
|
st);
|
|
g_signal_connect (G_OBJECT (gimage), "destroy",
|
|
G_CALLBACK (undo_history_gimage_destroy_callback),
|
|
st);
|
|
g_signal_connect (G_OBJECT (gimage), "clean",
|
|
G_CALLBACK (undo_history_clean_callback),
|
|
st);
|
|
|
|
/* The shell and main vbox */
|
|
{
|
|
gchar *basename;
|
|
gchar *title;
|
|
|
|
basename = g_path_get_basename (gimp_image_filename (gimage));
|
|
|
|
title = g_strdup_printf (_("Undo History: %s"), basename);
|
|
|
|
g_free (basename);
|
|
|
|
st->shell = gimp_dialog_new (title, "undo_history",
|
|
gimp_standard_help_func,
|
|
"dialogs/undo_history.html",
|
|
GTK_WIN_POS_NONE,
|
|
FALSE, TRUE, FALSE,
|
|
|
|
GTK_STOCK_CLOSE, undo_history_close_callback,
|
|
st, NULL, NULL, TRUE, TRUE,
|
|
|
|
NULL);
|
|
g_free (title);
|
|
}
|
|
|
|
vbox = gtk_vbox_new (FALSE, 2);
|
|
gtk_container_set_border_width (GTK_CONTAINER (vbox), 2);
|
|
gtk_container_add (GTK_CONTAINER (GTK_DIALOG (st->shell)->vbox), vbox);
|
|
gtk_widget_show (vbox);
|
|
|
|
g_signal_connect (G_OBJECT (st->shell), "destroy",
|
|
G_CALLBACK (undo_history_shell_destroy_callback),
|
|
st);
|
|
|
|
scrolled_win = gtk_scrolled_window_new (NULL, NULL);
|
|
gtk_widget_set_usize (GTK_WIDGET (scrolled_win),
|
|
160 + st->preview_size,
|
|
4 * (MAX (st->preview_size, 16) + 6));
|
|
|
|
/* clist of undo actions */
|
|
st->clist = gtk_clist_new (3);
|
|
gtk_clist_set_selection_mode (GTK_CLIST (st->clist), GTK_SELECTION_BROWSE);
|
|
gtk_clist_set_reorderable (GTK_CLIST (st->clist), FALSE);
|
|
gtk_clist_set_row_height (GTK_CLIST (st->clist), MAX (st->preview_size, 16) + 4);
|
|
gtk_clist_set_column_width (GTK_CLIST (st->clist), 0, st->preview_size + 2);
|
|
gtk_clist_set_column_width (GTK_CLIST (st->clist), 1, 18);
|
|
gtk_clist_set_column_min_width (GTK_CLIST (st->clist), 2, 64);
|
|
|
|
/* allocate the pixmaps if not already done */
|
|
if (!clean_pixmap)
|
|
{
|
|
GtkStyle *style;
|
|
|
|
gtk_widget_realize (st->shell);
|
|
style = gtk_widget_get_style (st->shell);
|
|
|
|
clean_pixmap =
|
|
gdk_pixmap_create_from_xpm_d (st->shell->window,
|
|
&clean_mask,
|
|
&style->bg[GTK_STATE_NORMAL],
|
|
yes_xpm);
|
|
|
|
clear_pixmap =
|
|
gdk_pixmap_create_from_xpm_d (st->shell->window,
|
|
&clear_mask,
|
|
&style->bg[GTK_STATE_NORMAL],
|
|
question_xpm);
|
|
}
|
|
|
|
/* work out the initial contents */
|
|
undo_map_over_undo_stack (st->gimage, undo_history_init_undo, st);
|
|
/* force selection to bottom */
|
|
gtk_clist_select_row (GTK_CLIST (st->clist),
|
|
GTK_CLIST (st->clist)->rows - 1, -1);
|
|
undo_map_over_redo_stack (st->gimage, undo_history_init_redo, st);
|
|
undo_history_prepend_special (GTK_CLIST (st->clist));
|
|
st->old_selection = GPOINTER_TO_INT(GTK_CLIST(st->clist)->selection->data);
|
|
|
|
/* draw the preview of the current state */
|
|
undo_history_set_pixmap (GTK_CLIST (st->clist),
|
|
st->old_selection, st->preview_size, st->gimage);
|
|
|
|
g_signal_connect (G_OBJECT (st->clist), "select_row",
|
|
G_CALLBACK (undo_history_select_row_callback),
|
|
st);
|
|
|
|
/* if the image is clean, set the clean pixmap */
|
|
if (st->gimage->dirty == 0)
|
|
gtk_clist_set_pixmap (GTK_CLIST (st->clist), st->old_selection, 1, clean_pixmap, clean_mask);
|
|
|
|
gtk_widget_show (GTK_WIDGET (st->clist));
|
|
|
|
gtk_box_pack_start (GTK_BOX (vbox), scrolled_win, TRUE, TRUE, 0);
|
|
gtk_widget_show (GTK_WIDGET (scrolled_win));
|
|
gtk_container_add (GTK_CONTAINER (scrolled_win), st->clist);
|
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
|
|
GTK_POLICY_NEVER,
|
|
GTK_POLICY_ALWAYS);
|
|
|
|
hbox = gtk_hbox_new (FALSE, 6);
|
|
gtk_container_set_border_width (GTK_CONTAINER (hbox), 2);
|
|
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
|
|
gtk_widget_show (hbox);
|
|
|
|
st->undo_button = button = gimp_pixmap_button_new (raise_xpm, _("Undo"));
|
|
gtk_box_pack_start (GTK_BOX (hbox), button, TRUE, TRUE, 0);
|
|
g_signal_connect (G_OBJECT (button), "clicked",
|
|
G_CALLBACK (undo_history_undo_callback),
|
|
st);
|
|
gtk_widget_show (GTK_WIDGET (button));
|
|
|
|
st->redo_button = button = gimp_pixmap_button_new (lower_xpm, _("Redo"));
|
|
gtk_box_pack_start (GTK_BOX (hbox), button, TRUE, TRUE, 0);
|
|
g_signal_connect (G_OBJECT (button), "clicked",
|
|
G_CALLBACK (undo_history_redo_callback),
|
|
st);
|
|
gtk_widget_show (GTK_WIDGET (button));
|
|
|
|
undo_history_set_sensitive (st, GTK_CLIST (st->clist)->rows);
|
|
|
|
gtk_widget_show (GTK_WIDGET (st->shell));
|
|
gtk_clist_moveto (GTK_CLIST (st->clist), st->old_selection, 0, 0.5, 0.0);
|
|
|
|
return st->shell;
|
|
}
|