/* The GIMP -- an image manipulation program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * newsprint plug-in
 * Copyright (C) 1997-1998 Austin Donnelly <austin@greenend.org.uk>
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * Portions of this plug-in are copyright 1991-1992 Adobe Systems
 * Incorporated.  See the spot_PS*() functions for details.
 */

/*
 * version 0.52
 * This version requires gtk-1.0.4 or above.
 *
 * This plug-in puts an image through a screen at a particular angle
 * and lines per inch, to arrive at a newspaper-style effect.
 *
 * Austin Donnelly <austin@greenend.org.uk>
 * http://www.cl.cam.ac.uk/~and1000/newsprint/
 *
 * Richard Mortier <rmm1002@cam.ac.uk> did the spot_round() function
 * with correct tonal balance.
 *
 * Tim Harris <tim.harris@acm.org> provided valuable feedback on
 * pre-press issues.
 *
 *
 * 0.52: 10 Jan 1999  <austin@greenend.org.uk>
 *    gtk_label_set() -> gtk_label_set_text()
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gtk/gtk.h>

#include <libgimp/gimp.h>

#ifdef RCSID
static char rcsid[] = "$Id$";
#endif

#define VERSION "v0.52"

/* Some useful macros */
#ifdef DEBUG
#define DEBUG_PRINT(x) printf x
#else
#define DEBUG_PRINT(x)
#endif

/* HACK so we compile with old gtks.  Won't work on machines where the
 * size of an int is different from the size of a void*, eg Alphas */
#ifndef GINT_TO_POINTER
#warning glib did not define GINT_TO_POINTER, assuming same size as int
#define GINT_TO_POINTER(x) ((gpointer)(x))
#endif

#ifndef GPOINTER_TO_INT
#warning glib did not define GPOINTER_TO_INT, assuming same size as int
#define GPOINTER_TO_INT(x) ((int)(x))
#endif


/*#define TIMINGS*/

#ifdef TIMINGS
#include <sys/time.h>
#include <unistd.h>
#define TM_INIT()	struct timeval tm_start, tm_end
#define TM_START()	gettimeofday(&tm_start, NULL)
#define TM_END()							 \
do {									 \
    gettimeofday(&tm_end, NULL);					 \
    tm_end.tv_sec  -= tm_start.tv_sec;					 \
    tm_end.tv_usec -= tm_start.tv_usec;					 \
    if (tm_end.tv_usec < 0)						 \
    {									 \
	tm_end.tv_usec += 1000000;					 \
	tm_end.tv_sec  -= 1;						 \
    }									 \
    printf("operation took %ld.%06ld\n", tm_end.tv_sec, tm_end.tv_usec); \
} while(0)
#else
#define TM_INIT()
#define TM_START()
#define TM_END()
#endif

#define TILE_CACHE_SIZE 16
#define ENTSCALE_SCALE_WIDTH 125
#define ENTSCALE_ENTRY_WIDTH 40
#define DEF_OVERSAMPLE	1	/* default for how much to oversample by */
#define SPOT_PREVIEW_SZ	16
#define PREVIEW_OVERSAMPLE 3


#define BOUNDS(a,x,y)	((a < x) ? x : ((a > y) ? y : a))
#define ISNEG(x)	(((x) < 0)? 1 : 0)
#define DEG2RAD(d)	((d) * G_PI / 180)
#define VALID_BOOL(x)	((x) == TRUE || (x) == FALSE)
#define CLAMPED_ADD(a, b) (((a)+(b) > 0xff)? 0xff : (a) + (b))

/* Ideally, this would be in a common header file somewhere.  This was
 * nicked from app/convert.c */
#define INTENSITY(r,g,b) (r * 0.30 + g * 0.59 + b * 0.11 + 0.001)

/* Bartlett window supersampling weight function.  See table 4.1, page
 * 123 of Alan Watt and Mark Watt, Advanced Animation and Rendering
 * Techniques, theory and practice. Addison-Wesley, 1992. ISBN
 * 0-201-54412-1 */
#define BARTLETT(x,y)	(((oversample/2)+1-ABS(x)) * ((oversample/2)+1-ABS(y)))
#define WGT(x,y)	wgt[((y+oversample/2)*oversample) + x+oversample/2]

/* colourspaces we can separate to: */
#define CS_GREY		0
#define CS_RGB		1
#define CS_CMYK		2
#define CS_INTENSITY	3
#define NUM_CS		4
#define VALID_CS(x)	((x) >= 0 && (x) <= NUM_CS-1)

/* Spot function related types and definitions */

typedef gdouble spotfn_t (gdouble x, gdouble y);

/* forward declaration of the functions themselves */
static spotfn_t spot_round;
static spotfn_t spot_line;
static spotfn_t spot_diamond;
static spotfn_t spot_PSsquare;
static spotfn_t spot_PSdiamond;

typedef struct {
    const char	*name;		/* function's name */
    spotfn_t	*fn;		/* function itself */
    guchar	*thresh;	/* cached threshold matrix */
    gdouble	prev_lvl[3];	/* intensities to preview */
    guchar	*prev_thresh;	/* preview-sized threshold matrix */
    gint	balanced;	/* TRUE if spot fn is already balanced */
} spot_info_t;


/* This is all the info needed per spot function.  Functions are refered to
 * by their index into this array. */
static spot_info_t spotfn_list[] = {
#define SPOTFN_DOT 0
    {"round",
     spot_round,
     NULL,
     {.22, .50, .90},
     NULL,
     FALSE},

    {"line",
     spot_line,
     NULL,
     {.15, .50, .80},
     NULL,
     FALSE},

    {"diamond",
     spot_diamond,
     NULL,
     {.15, .50, .80},
     NULL,
     TRUE},

    {"PS square (Euclidean dot)",
     spot_PSsquare,
     NULL,
     {.15, .50, .90},
     NULL,
     FALSE},

    {"PS diamond",
     spot_PSdiamond,
     NULL,
     {.15, .50, .90},
     NULL,
     FALSE},

    /* NULL-name terminates */
    {NULL,
     NULL,
     NULL,
     {0.0, 0.0, 0.0},
     NULL,
     FALSE}
};

#define NUM_SPOTFN	((sizeof(spotfn_list) / sizeof(spot_info_t)) - 1)
#define VALID_SPOTFN(x)	((x) >= 0 && (x) < NUM_SPOTFN)
#define THRESH(x,y)	(thresh[(y)*width + (x)])
#define THRESHn(n,x,y)	((thresh[n])[(y)*width + (x)])
    


/* Arguments to filter */

/* Some of these are here merely to save them across calls.  They are
 * marked as "UI use".  Everything else is a valid arg. */
typedef struct {
    /* resolution section: */
    gint	cell_width;

    /* screening section: */
    gint	colourspace;	/* 0: RGB, 1: CMYK, 2: Intensity */
    gint	k_pullout;	/* percentage of black to pull out */

    /* grey screen (only used if greyscale drawable) */
    gdouble	gry_ang;
    gint	gry_spotfn;
    /* red / cyan screen */
    gdouble	red_ang;
    gint	red_spotfn;
    /* green / magenta screen */
    gdouble	grn_ang;
    gint	grn_spotfn;
    /* blue / yellow screen */
    gdouble	blu_ang;
    gint	blu_spotfn;

    /* anti-alias section */
    gint	oversample;	/* 1 == no anti-aliasing, else small odd int */
} NewsprintValues;

/* bits of state used by the UI, but not visible from the PDB */
typedef struct {
    gint	input_spi;	/* input samples per inch */
    gdouble	output_lpi;	/* desired output lines per inch */
    gint	lock_channels;	/* changes to one channel affect all */
} NewsprintUIValues;

typedef struct {
    gint	run;
} NewsprintInterface;


typedef void (*EntscaleCallbackFunc) (gdouble new_val, gpointer data);


typedef enum {
  ENTSCALE_INT,
  ENTSCALE_DOUBLE
} EntscaleType;


typedef struct {
  GtkObject		*adjustment;
  GtkWidget		*entry;
  GtkWidget		*label;
  GtkWidget		*hbox;
  EntscaleType		type;
  gchar			fmt_string[16];
  gint			constraint;
  EntscaleCallbackFunc	callback;
  gpointer		call_data;
} Entscale;




/* state for the preview widgets */
typedef struct {
    GtkWidget	*widget;	/* preview widget itself */
    GtkWidget	*label;		/* the label below it */
} preview_st;

/* state for the channel frames */
typedef struct _channel_st {
    GtkWidget	*frame;		/* frame around this channel */
    gint	*spotfn_num;	/* which spotfn the menu is controlling */
    preview_st	prev[3];	/* state for 3 preview widgets */
    Entscale	*entscale;	/* angle entscale widget */
    GtkWidget	*option_menu;	/* popup for spot function */
    GtkWidget	*menuitem[NUM_SPOTFN];	/* menuitems for each spot function */
    GtkWidget	*ch_menuitem;	/* menuitem for the channel selector */
    gint	ch_menu_num;	/* this channel's position in the selector */
    struct _channel_st *next;	/* circular list of channels in locked group */
} channel_st;


/* State associated with the configuration dialog and passed to its
 * callback functions */
typedef struct {
    GtkWidget	*dlg;	/* main dialog itself */
    Entscale	*pull;	/* black pullout percentage */
    Entscale	*input_spi;
    Entscale	*output_lpi;
    Entscale	*cellsize;
    GtkWidget	*vbox;	/* container for screen info */    
    GtkWidget	*current_ch; /* which channel is currently being edited */
    GtkWidget	*channel_menu;		/* menu of channels */
    GtkWidget	*channel_option;	/* option menu for channels */
    /* room for up to 4 channels per colourspace */
    channel_st	*chst[NUM_CS][4];
} NewsprintDialog_st;




/***** Local vars *****/


/* defaults */
/* fixed copy so we can reset them */
static const NewsprintValues factory_defaults =
{
    /* resolution stuff */
    10,		/* cell width */

    /* screen setup (default is the classic rosette pattern) */
    CS_RGB,	/* use RGB, not CMYK or Intensity */
    100,	/* max pullout */
    /* grey/black */
    45.0,
    SPOTFN_DOT,
    /* red/cyan */
    15.0,
    SPOTFN_DOT,
    /* green/magenta */
    75.0,
    SPOTFN_DOT,
    /* blue/yellow */
    0.0,
    SPOTFN_DOT,

    /* anti-alias control */
    DEF_OVERSAMPLE
};

static const NewsprintUIValues factory_defaults_ui = {
    72,		/* input spi */
     7.2,	/* output lpi */
    FALSE	/* lock channels */
};

/* Mutable copy for normal use.  Initialised in run(). */
static NewsprintValues pvals;
static NewsprintUIValues pvals_ui;

static NewsprintInterface pint =
{
  FALSE	    /* run */
};


/* channel templates */
typedef struct {
    const gchar	*name;
    /* pointers to the variables this channel updates */
    gdouble	*angle;
    gint	*spotfn;
    /* factory defaults for the angle and spot function (as pointers so
     * the silly compiler can see they're really constants) */
    const gdouble *factory_angle;
    const gint	*factory_spotfn;
} chan_tmpl;

static const chan_tmpl grey_tmpl[] = {
    {"Grey",
     &pvals.gry_ang,
     &pvals.gry_spotfn,
     &factory_defaults.gry_ang,
     &factory_defaults.gry_spotfn},

    {NULL, NULL, NULL, NULL, NULL}
};

static const chan_tmpl rgb_tmpl[] = {
    {"Red",
     &pvals.red_ang,
     &pvals.red_spotfn,
     &factory_defaults.red_ang,
     &factory_defaults.red_spotfn},

    {"Green",
     &pvals.grn_ang,
     &pvals.grn_spotfn,
     &factory_defaults.grn_ang,
     &factory_defaults.grn_spotfn},

    {"Blue",
     &pvals.blu_ang,
     &pvals.blu_spotfn,
     &factory_defaults.blu_ang,
     &factory_defaults.blu_spotfn},

    {NULL, NULL, NULL, NULL, NULL}
};

static const chan_tmpl cmyk_tmpl[] = {
    {"Cyan",
     &pvals.red_ang,
     &pvals.red_spotfn,
     &factory_defaults.red_ang,
     &factory_defaults.red_spotfn},


    {"Magenta",
     &pvals.grn_ang,
     &pvals.grn_spotfn,
     &factory_defaults.grn_ang,
     &factory_defaults.grn_spotfn},

    {"Yellow",
     &pvals.blu_ang,
     &pvals.blu_spotfn,
     &factory_defaults.blu_ang,
     &factory_defaults.blu_spotfn},

    {"Black",
     &pvals.gry_ang,
     &pvals.gry_spotfn,
     &factory_defaults.gry_ang,
     &factory_defaults.gry_spotfn},

    {NULL, NULL, NULL, NULL, NULL}
};

static const chan_tmpl intensity_tmpl[] = {
    {"Intensity",
     &pvals.gry_ang,
     &pvals.gry_spotfn,
     &factory_defaults.gry_ang,
     &factory_defaults.gry_spotfn},

    {NULL, NULL, NULL, NULL, NULL}
};

/* cspace_chan_tmpl is indexed by colourspace, and gives an array of
 * channel templates for that colourspace */
static const chan_tmpl *cspace_chan_tmpl[] = {
    grey_tmpl,
    rgb_tmpl,
    cmyk_tmpl,
    intensity_tmpl
};

#define NCHANS(x) ((sizeof(x) / sizeof(chan_tmpl)) - 1)

/* cspace_nchans gives a quick way of finding the number of channels
 * in a colourspace.  Alternatively, if you're walking the channel
 * template, you can use the NULL entry at the end to stop. */
static const gint cspace_nchans[] = {
    NCHANS(grey_tmpl),
    NCHANS(rgb_tmpl),
    NCHANS(cmyk_tmpl),
    NCHANS(intensity_tmpl)
};


/* Declare local functions.  */
static void	query	(void);
static void	run	(gchar	 *name,
			 gint	 nparams,
			 GParam	 *param,
			 gint	 *nreturn_vals,
			 GParam	 **return_vals);

static gint	newsprint_dialog (GDrawable *drawable);
static void	newsprint_close_callback	 (GtkWidget *widget,
					  gpointer   data);
static void	newsprint_ok_callback	 (GtkWidget *widget,
					  gpointer   data);

static void	newsprint_toggle_update (GtkWidget *widget,
					 gpointer   data);

static void	newsprint_cspace_update (GtkWidget *widget,
					 gpointer   data);

static void	newsprint	(GDrawable *drawable);

static guchar *	spot2thresh	(gint type, gint width);

static Entscale * entscale_new (GtkWidget *table, gint x, gint y,
			  gchar *caption, EntscaleType type, gpointer variable,
			  gdouble min, gdouble max, gdouble step,
			  gint constraint, EntscaleCallbackFunc callback,
			  gpointer call_data);


static int entscale_get_precision (gdouble step);
static void entscale_destroy_callback (GtkWidget *widget, gpointer data);
static void entscale_scale_update (GtkAdjustment *adjustment, gpointer data);
static void entscale_entry_update (GtkWidget *widget, gpointer data);

static void entscale_set_sensitive (Entscale *ent, gint sensitive);
static void entscale_set_value(Entscale *ent, gfloat value);



GPlugInInfo PLUG_IN_INFO =
{
  NULL,	   /* init_proc */
  NULL,	   /* quit_proc */
  query,   /* query_proc */
  run	   /* run_proc */
};



/***** Functions *****/

MAIN ()

static void
query()
{
  static GParamDef args[]=
    {
      { PARAM_INT32, "run_mode", "Interactive, non-interactive" },
      { PARAM_IMAGE, "image", "Input image (unused)" },
      { PARAM_DRAWABLE, "drawable", "Input drawable" },

      { PARAM_INT32, "cell_width", "screen cell width, in pixels" },

      { PARAM_INT32, "colourspace", "separate to 0:RGB, 1:CMYK, 2:Intensity" },
      { PARAM_INT32, "k_pullout", "Percentage of black to pullout (CMYK only)" },

      { PARAM_FLOAT, "gry_ang", "Grey/black screen angle (degrees)" },
      { PARAM_INT32, "gry_spotfn", "Grey/black spot function (0=dots, 1=lines, 2=diamonds, 3=euclidean dot, 4=PS diamond)" },
      { PARAM_FLOAT, "red_ang", "Red/cyan screen angle (degrees)" },
      { PARAM_INT32, "red_spotfn", "Red/cyan spot function (values as gry_spotfn)" },
      { PARAM_FLOAT, "grn_ang", "Green/magenta screen angle (degrees)" },
      { PARAM_INT32, "grn_spotfn", "Green/magenta spot function (values as gry_spotfn)" },
      { PARAM_FLOAT, "blu_ang", "Blue/yellow screen angle (degrees)" },
      { PARAM_INT32, "blu_spotfn", "Blue/yellow spot function (values as gry_spotfn)" },
      
      { PARAM_INT32, "oversample", "how many times to oversample spot fn" }
      /* 15 args */
   };
  static GParamDef *return_vals = NULL;
  static gint nargs = sizeof (args) / sizeof (args[0]);
  static gint nreturn_vals = 0;

  gimp_install_procedure ("plug_in_newsprint",
			  "Re-sample the image to give a newspaper-like "
			  "effect",
			  "Halftone the image, trading off "
			  "resolution to represent colours or grey levels "
			  "using the process described both in the PostScript "
			  "language definition, and also by Robert Ulichney, "
			  "\"Digital halftoning\", MIT Press, 1987.",
			  "Austin Donnelly",
			  "Austin Donnelly",
			  "1998 (" VERSION ")",
			  "<Image>/Filters/Render/Newsprint",
			  "RGB*, GRAY*",
			  PROC_PLUG_IN,
			  nargs, nreturn_vals,
			  args, return_vals);
}

static void
run (gchar   *name,
     gint    nparams,
     GParam  *param,
     gint    *nreturn_vals,
     GParam  **return_vals)
{
  static GParam values[1];
  GDrawable *drawable;
  GRunModeType run_mode;
  GStatusType status = STATUS_SUCCESS;

  run_mode = param[0].data.d_int32;

  *nreturn_vals = 1;
  *return_vals = values;

  values[0].type = PARAM_STATUS;
  values[0].data.d_status = status;

  /* basic defaults */
  pvals    = factory_defaults;
  pvals_ui = factory_defaults_ui;

  /*  Get the specified drawable  */
  drawable = gimp_drawable_get (param[2].data.d_drawable);

  switch (run_mode)
  {
  case RUN_INTERACTIVE:
      /*  Possibly retrieve data  */
      gimp_get_data ("plug_in_newsprint", &pvals);
      gimp_get_data ("plug_in_newsprint_ui", &pvals_ui);

      /*  First acquire information with a dialog  */
      if (! newsprint_dialog (drawable))
      {
	  gimp_drawable_detach (drawable);
	  return;
      }
      break;

    case RUN_NONINTERACTIVE:
	/*  Make sure all the arguments are there!  */
	if (nparams != 15)
	{
	    status = STATUS_CALLING_ERROR;
	    break;
	}

	pvals.cell_width = param[3].data.d_int32;
	pvals.colourspace = param[4].data.d_int32;
	pvals.k_pullout  = param[5].data.d_int32;
	pvals.gry_ang	 = param[6].data.d_float;
	pvals.gry_spotfn = param[7].data.d_int32;
	pvals.red_ang    = param[8].data.d_float;
	pvals.red_spotfn = param[9].data.d_int32;
	pvals.grn_ang    = param[10].data.d_float;
	pvals.grn_spotfn = param[11].data.d_int32;
	pvals.blu_ang    = param[12].data.d_float;
	pvals.blu_spotfn = param[13].data.d_int32;
	pvals.oversample = param[14].data.d_int32;

	/* check values are within permitted ranges */
	if (!VALID_SPOTFN(pvals.gry_spotfn) ||
	    !VALID_SPOTFN(pvals.red_spotfn) ||
	    !VALID_SPOTFN(pvals.grn_spotfn) ||
	    !VALID_SPOTFN(pvals.blu_spotfn) ||
	    !VALID_CS(pvals.colourspace) ||
	    pvals.k_pullout < 0 || pvals.k_pullout > 100)
	    status = STATUS_CALLING_ERROR;

	break;

  case RUN_WITH_LAST_VALS:
      /*  Possibly retrieve data  */
      gimp_get_data ("plug_in_newsprint", &pvals);
      break;

  default:
      break;
  }

  if (status == STATUS_SUCCESS)
  {
      /*  Make sure that the drawable is gray or RGB color  */
      if (gimp_drawable_color (drawable->id) ||
	  gimp_drawable_gray (drawable->id))
      {
	  gimp_progress_init("Newsprintifing...");

	  /*  set the tile cache size  */
	  gimp_tile_cache_ntiles(TILE_CACHE_SIZE);

	  /*  run the newsprint effect  */
	  newsprint(drawable);

	  if (run_mode != RUN_NONINTERACTIVE)
	      gimp_displays_flush();

	  /*  Store data  */
	  if (run_mode == RUN_INTERACTIVE)
	  {
	      gimp_set_data ("plug_in_newsprint",
			     &pvals, sizeof (NewsprintValues));
	      gimp_set_data ("plug_in_newsprint_ui",
			     &pvals_ui, sizeof (NewsprintUIValues));
	  }
      }
      else
      {
	  /*gimp_message ("newsprint: cannot operate on indexed images");*/
	  status = STATUS_EXECUTION_ERROR;
      }
  }

  values[0].data.d_status = status;
  
  gimp_drawable_detach(drawable);
}




/* create new menu state, and the preview widgets for it */
static channel_st *
new_preview(gint *sfn)
{
    channel_st	*st;
    GtkWidget	*preview;
    GtkWidget	*label;
    gint	i;

    st = g_new(channel_st, 1);

    st->spotfn_num = sfn;

    /* make the preview widgets */
    for(i=0; i < 3; i++)
    {
	preview = gtk_preview_new(GTK_PREVIEW_COLOR);
	gtk_preview_size(GTK_PREVIEW(preview),
			 SPOT_PREVIEW_SZ*2 + 1, SPOT_PREVIEW_SZ*2 + 1);
	gtk_widget_show(preview);

	label = gtk_label_new("");
	gtk_widget_show(label);
	
	st->prev[i].widget = preview;
	st->prev[i].label  = label;
	/* st->prev[i].value changed by preview_update */
    }

    return st;
}


/* the popup menu "st" has changed, so the previews associated with it 
 * need re-calculation */
static void
preview_update(channel_st *st)
{
    gint	sfn = *(st->spotfn_num);
    preview_st	*prev;
    gint	i;
    gint	x;
    gint	y;
    gint	width = SPOT_PREVIEW_SZ * PREVIEW_OVERSAMPLE;
    gint	oversample = PREVIEW_OVERSAMPLE;
    guchar	*thresh;
    gchar	pct[12];
    guchar	value;
    guchar	row[3 * (2 * SPOT_PREVIEW_SZ + 1)];

    /* If we don't yet have a preview threshold matrix for this spot
     * function, generate one now. */
    if (!spotfn_list[sfn].prev_thresh)
    {
	spotfn_list[sfn].prev_thresh =
	    spot2thresh(sfn, SPOT_PREVIEW_SZ*PREVIEW_OVERSAMPLE);
    }


    thresh = spotfn_list[sfn].prev_thresh;

    for(i=0; i < 3; i++)
    {
	prev = &st->prev[i];

	value = spotfn_list[sfn].prev_lvl[i] * 0xff;

	for (y=0; y <= SPOT_PREVIEW_SZ*2; y++)
	{
	    for (x=0; x <= SPOT_PREVIEW_SZ*2; x++)
	    {
		guint32 sum = 0;
		gint sx, sy;
		gint tx, ty;
		gint rx, ry;

		rx = x * PREVIEW_OVERSAMPLE;
		ry = y * PREVIEW_OVERSAMPLE;

		for(sy=-oversample/2; sy<=oversample/2; sy++)
		    for(sx=-oversample/2; sx<=oversample/2; sx++)
		    {
			tx = rx+sx;
			ty = ry+sy;
			while(tx < 0)  tx += width;
			while(ty < 0)  ty += width;
			while(tx >= width)  tx -= width;
			while(ty >= width)  ty -= width;
			
			if (value > THRESH(tx, ty))
			    sum += 0xff * BARTLETT(sx, sy);
		    }
		sum /= BARTLETT(0,0) * BARTLETT(0,0);

		/* blue lines on cell boundaries */
		if ((x % SPOT_PREVIEW_SZ) == 0 ||
		    (y % SPOT_PREVIEW_SZ) == 0)
		{
		    row[x*3+0] = 0;
		    row[x*3+1] = 0;
		    row[x*3+2] = 0xff;
		}
		else
		{
		    row[x*3+0] = sum;
		    row[x*3+1] = sum;
		    row[x*3+2] = sum;
		}
	    }

	    gtk_preview_draw_row(GTK_PREVIEW(prev->widget),
				 row, 0, y, SPOT_PREVIEW_SZ*2+1);
}

	/* redraw preview widget */
	gtk_widget_draw(prev->widget, NULL);

	sprintf(pct, "%2d%%", (int)RINT(spotfn_list[sfn].prev_lvl[i] * 100));
	gtk_label_set_text (GTK_LABEL(prev->label), pct);
    }

    gdk_flush ();
}

    
static void
newsprint_menu_callback(GtkWidget *widget,
			gpointer   data)
{
    channel_st *st = data;
    gpointer ud;
    gint menufn;
    static gint in_progress = FALSE;

    /* we shouldn't need recursion protection, but if lock_channels is
     * set, and gtk_option_menu_set_history ever generates an
     * "activated" signal, then we'll get back here.  So we've defensive. */
    if (in_progress)
    {
	printf("newsprint_menu_callback: unexpected recursion: "
	       "can't happen\n");
	return;
    }	

    in_progress = TRUE;

    ud = gtk_object_get_user_data(GTK_OBJECT(widget));
    menufn = GPOINTER_TO_INT(ud);

    *(st->spotfn_num) = menufn;

    preview_update(st);

    /* ripple the change to the other popups if the channels are
     * locked together. */
    if (pvals_ui.lock_channels)
    {
	channel_st *c = st->next;
	gint	oldfn;

	while(c != st)
	{
	    gtk_option_menu_set_history(GTK_OPTION_MENU(c->option_menu),
					menufn);
	    oldfn = *(c->spotfn_num);
	    *(c->spotfn_num) = menufn;
	    if (oldfn != menufn)
		preview_update(c);
	    c = c->next;
	}
    }

    in_progress = FALSE;
}


static void
angle_callback(gdouble new_val, gpointer data)
{
    channel_st *st = data;
    channel_st *c;
    static gint in_progress = FALSE;

    if (pvals_ui.lock_channels && !in_progress)
    {
	in_progress = TRUE;

	c = st->next;

	while(c != st)
	{
	    entscale_set_value(c->entscale, new_val);
	    c = c->next;
	}

	in_progress = FALSE;
    }
}


static void
spi_callback(gdouble new_val, gpointer data)
{
    NewsprintDialog_st *st = data;
    Entscale *save;

    save = st->input_spi;
    st->input_spi = NULL;
    
    if (st->output_lpi)
	entscale_set_value(st->output_lpi, new_val / pvals.cell_width);

    st->input_spi = save;
}

static void
lpi_callback(gdouble new_val, gpointer data)
{
    NewsprintDialog_st *st = data;
    Entscale *save;

    save = st->output_lpi;
    st->output_lpi = NULL;
    
    if (st->cellsize)
	entscale_set_value(st->cellsize, pvals_ui.input_spi / new_val);

    st->output_lpi = save;
}

static void
cellsize_callback(gdouble new_val, gpointer data)
{
    NewsprintDialog_st *st = data;
    Entscale *save;

    save = st->cellsize;
    st->cellsize = NULL;
    
    if (st->output_lpi)
	entscale_set_value(st->output_lpi,
			   pvals_ui.input_spi / pvals.cell_width);

    st->cellsize = save;
}


static void
newsprint_channel_select_callback(GtkWidget *widget,
				  gpointer  data)
{
    NewsprintDialog_st *st = data;
    channel_st *chst;

    chst = gtk_object_get_user_data(GTK_OBJECT(widget));

    /* hide the current channel, and show our channel */
    if (st->current_ch)
	gtk_widget_hide(st->current_ch);

    gtk_widget_show(chst->frame);

    st->current_ch = chst->frame;
}


static void
newsprint_defaults_callback(GtkWidget *widget,
			    gpointer   data)
{
    NewsprintDialog_st *st = data;
    gint	saved_lock;
    gint	cspace;
    channel_st	**chst;
    const chan_tmpl *ct;
    gint	spotfn;
    gint	c;

    /* temporarily turn off channel lock */
    saved_lock = pvals_ui.lock_channels;
    pvals_ui.lock_channels = FALSE;

    /* for each colourspace, reset its channel info */
    for(cspace=0; cspace < NUM_CS; cspace++)
    {
	chst = st->chst[cspace];
	ct = cspace_chan_tmpl[cspace];

	/* skip this colourspace if we haven't used it yet */
	if (!chst[0])
	    continue;

	c = 0;
	while(ct->name)
	{
	    entscale_set_value(chst[c]->entscale, *(ct->factory_angle));

	    /* change the popup menu and also activate the menuitem in
	     * question, in order to run the handler that re-computes
	     * the preview area */
	    spotfn = *(ct->factory_spotfn);
	    gtk_option_menu_set_history(GTK_OPTION_MENU(chst[c]->option_menu),
					spotfn);
	    gtk_menu_item_activate(GTK_MENU_ITEM(chst[c]->menuitem[spotfn]));

	    c++;
	    ct++;
	}
    }

    pvals_ui.lock_channels = saved_lock;
}

/* Create (but don't yet show) a new frame for a channel 'widget'.
 * Return the channel state, so caller can find the frame and place it
 * in a container. */
static channel_st *
new_channel(const chan_tmpl *ct)
{
    GtkWidget *table;
    GtkWidget *label;
    GtkWidget *menu;
    spot_info_t *sf;
    channel_st *chst;
    gint      i;    

    /* create the channel state record */
    chst = new_preview(ct->spotfn);

    chst->frame = gtk_frame_new (ct->name);
    gtk_frame_set_shadow_type (GTK_FRAME (chst->frame), GTK_SHADOW_ETCHED_IN);
    gtk_container_border_width (GTK_CONTAINER (chst->frame), 4);

    table = gtk_table_new (3, 2, FALSE);
    gtk_container_border_width (GTK_CONTAINER (table), 4);
    gtk_container_add (GTK_CONTAINER (chst->frame), table);

    /* angle slider */
    chst->entscale = entscale_new(table, 0, 0, "angle",
				  ENTSCALE_DOUBLE, ct->angle,
				  -90.0, 90.0, 1.0, TRUE/*constrain*/,
				  angle_callback, chst);

    /* spot function popup */
    label = gtk_label_new("spot function");
    gtk_table_attach(GTK_TABLE(table), label, 0, 1, 1, 2,
		     GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);
    gtk_widget_show(label);

    chst->option_menu = gtk_option_menu_new();
    gtk_table_attach(GTK_TABLE(table), chst->option_menu, 0, 1, 2, 3,
		     GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
    gtk_widget_show(chst->option_menu);

    menu = gtk_menu_new();

    sf = spotfn_list;
    i = 0;
    while(sf->name)
    {
	chst->menuitem[i] = gtk_menu_item_new_with_label(sf->name);
	gtk_signal_connect(GTK_OBJECT(chst->menuitem[i]), "activate",
			   (GtkSignalFunc) newsprint_menu_callback,
			   chst);
	gtk_object_set_user_data(GTK_OBJECT(chst->menuitem[i]),
				 GINT_TO_POINTER(i));
	gtk_widget_show(chst->menuitem[i]);
	gtk_menu_append(GTK_MENU(menu), GTK_WIDGET(chst->menuitem[i]));
	sf++;
	i++;
    }

    gtk_menu_set_active(GTK_MENU(menu), *ct->spotfn);

    gtk_option_menu_set_menu(GTK_OPTION_MENU(chst->option_menu), menu);
    gtk_widget_show(chst->option_menu);

    /* spot function previews go in table slots 1-2, 1-3 (double
     * height) */
    {
	GtkWidget *sub;
	GtkWidget *align;

	sub = gtk_table_new(2, 3, FALSE);
	gtk_container_border_width(GTK_CONTAINER(sub), 1);

	align = gtk_alignment_new(0.5, 1.0, 1.0, 0.0);
	gtk_container_add(GTK_CONTAINER(align), sub);
	gtk_widget_show(align);

	gtk_table_attach(GTK_TABLE(table), align, 1, 2, 1, 3,
			 GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);

	for(i=0; i < 3; i++)
	{
	    gtk_table_attach(GTK_TABLE(sub), chst->prev[i].widget,
			     i, i+1, 0, 1,
			     GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);

	    gtk_table_attach(GTK_TABLE(sub), chst->prev[i].label,
			     i, i+1, 1, 2,
			     GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);
	}

	gtk_widget_show(sub);
    }

    /* fire the update once to make sure we start with something
     * in the preview windows */
    preview_update(chst);

    gtk_widget_show(table);

    /* create the menuitem used to select this channel for editing */
    chst->ch_menuitem = gtk_menu_item_new_with_label(ct->name);
    gtk_object_set_user_data(GTK_OBJECT(chst->ch_menuitem), chst);
    /* signal attachment and showing left to caller */

    /* deliberately don't show the chst->frame, leave that up to
     * the caller */

    return chst;
}


/* Make all the channels needed for "colourspace", and fill in
 * the respective channel state fields in "st". */
static void
gen_channels(NewsprintDialog_st *st, gint colourspace)
{
    static int cur_menu_num = 0;
    const chan_tmpl *ct;
    channel_st	**chst;
    channel_st	*base = NULL;
    gint	i;

    chst = st->chst[colourspace];
    ct   = cspace_chan_tmpl[colourspace];
    i    = 0;

    while(ct->name)
    {
	chst[i] = new_channel(ct);

	gtk_signal_connect(GTK_OBJECT(chst[i]->ch_menuitem), "activate",
			   (GtkSignalFunc)newsprint_channel_select_callback,
			   st);

	/* only link in the menuitem if we're doing multiple channels */
	if (st->channel_menu)
	{
	    gtk_menu_append(GTK_MENU(st->channel_menu),
			    GTK_WIDGET(chst[i]->ch_menuitem));
	    chst[i]->ch_menu_num = cur_menu_num;
	    cur_menu_num++;
	}

	if (i)
	    chst[i-1]->next = chst[i];
	else
	    base = chst[i];

	gtk_box_pack_start(GTK_BOX(st->vbox), chst[i]->frame,
			   TRUE, FALSE, 0);
	gtk_box_reorder_child(GTK_BOX(st->vbox), chst[i]->frame, 5+i);

	i++;
	ct++;
    }

    /* make the list circular */
    chst[i-1]->next = base;
}


static gint
newsprint_dialog (GDrawable *drawable)
{
    gchar	**argv;
    gint	argc;
    /* widgets we need from callbacks stored here */
    NewsprintDialog_st	st;
    GtkWidget	*frame;
    GtkWidget	*table;
    GtkWidget	*align;
    GtkWidget	*button;
    GtkWidget	*hbox;
    GtkWidget	*toggle;
    GtkWidget	*label;
    GSList	*group = NULL;
    gint	bpp;
    guchar	*color_cube;
    gint	i;

    argc = 1;
    argv = g_new(gchar *, 1);
    argv[0] = g_strdup("newsprint");

    gtk_init (&argc, &argv);
    gtk_rc_parse(gimp_gtkrc());

    gtk_preview_set_gamma (gimp_gamma ());
    gtk_preview_set_install_cmap (gimp_install_cmap ());
    color_cube = gimp_color_cube ();
    gtk_preview_set_color_cube (color_cube[0], color_cube[1],
				color_cube[2], color_cube[3]);

    gtk_widget_set_default_visual (gtk_preview_get_visual ());
    gtk_widget_set_default_colormap (gtk_preview_get_cmap ());

#if 0
    printf("newsprint: waiting... (pid %d)\n", getpid());
    kill(getpid(), 19);
#endif

    /* flag values to say we haven't filled these channel
     * states in yet */
    for(i=0; i<NUM_CS; i++)
	st.chst[i][0] = NULL;

    /* we haven't shown any channels yet */
    st.current_ch = NULL;

    /* need to know the bpp, so we can tell if we're doing 
     * RGB/CMYK or grey style of dialog box */
    bpp = gimp_drawable_bpp(drawable->id);
    if (gimp_drawable_has_alpha(drawable->id))
	bpp--;	

    /* force greyscale if it's the only thing we can do */
    if (bpp == 1) {
	pvals.colourspace = CS_GREY;
    } else {
	if (pvals.colourspace == CS_GREY)
	    pvals.colourspace = CS_RGB;
    }

    st.dlg = gtk_dialog_new ();
    gtk_window_set_title (GTK_WINDOW (st.dlg), "Newsprint");
    gtk_window_position (GTK_WINDOW (st.dlg), GTK_WIN_POS_MOUSE);
    gtk_signal_connect (GTK_OBJECT (st.dlg), "destroy",
			(GtkSignalFunc) newsprint_close_callback,
			NULL);

    /*  Action area  */
    button = gtk_button_new_with_label ("OK");
    GTK_WIDGET_SET_FLAGS (button, GTK_CAN_DEFAULT);
    gtk_signal_connect (GTK_OBJECT (button), "clicked",
			(GtkSignalFunc) newsprint_ok_callback,
			st.dlg);
    gtk_box_pack_start (GTK_BOX (GTK_DIALOG (st.dlg)->action_area), button,
			TRUE, TRUE, 0);
    gtk_widget_grab_default (button);
    gtk_widget_show (button);

    button = gtk_button_new_with_label ("Cancel");
    GTK_WIDGET_SET_FLAGS (button, GTK_CAN_DEFAULT);
    gtk_signal_connect_object (GTK_OBJECT (button), "clicked",
			       (GtkSignalFunc) gtk_widget_destroy,
			       GTK_OBJECT (st.dlg));
    gtk_box_pack_start (GTK_BOX (GTK_DIALOG (st.dlg)->action_area), button,
			TRUE, TRUE, 0);
    gtk_widget_show (button);

    /* resolution settings  */
    frame = gtk_frame_new ("Resolution");
    gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN);
    gtk_container_border_width (GTK_CONTAINER (frame), 10);
    gtk_box_pack_start (GTK_BOX (GTK_DIALOG (st.dlg)->vbox), frame,
			TRUE, TRUE, 0);

    table = gtk_table_new (3, 2, FALSE);
    gtk_container_border_width (GTK_CONTAINER (table), 10);
    gtk_container_add (GTK_CONTAINER (frame), table);

#ifdef GIMP_HAVE_RESOLUTION_INFO
    {
      double xres, yres;
      gimp_image_get_resolution(gimp_drawable_image_id(drawable->id),
				&xres, &yres);
      /* XXX hack: should really note both resolutions, and use
       * rectangular cells, not square cells.  But I'm being lazy,
       * and the majority of the world works with xres == yres */
      pvals_ui.input_spi = xres;
    }
#endif

    st.input_spi  = NULL;
    st.output_lpi = NULL;
    st.cellsize   = NULL;

    st.input_spi = entscale_new(table, 0, 0, "Input SPI ",
				ENTSCALE_INT, &pvals_ui.input_spi,
				1.0, 1200.0, 5.0, FALSE/*constrain*/,
				spi_callback, &st);

    st.output_lpi = entscale_new(table, 0, 1, "Output LPI ",
				 ENTSCALE_DOUBLE, &pvals_ui.output_lpi,
				 1.0, 1200.0, 5.0, FALSE/*constrain*/,
				 lpi_callback, &st);

    st.cellsize = entscale_new(table, 0, 2, "Cell size ",
			       ENTSCALE_INT, &pvals.cell_width,
			       3.0, 100.0, 1.0, FALSE/*constrain*/,
			       cellsize_callback, &st);

    gtk_widget_show(table);
    gtk_widget_show(frame);


    /* screen settings */
    frame = gtk_frame_new ("Screen");
    gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN);
    gtk_container_border_width (GTK_CONTAINER (frame), 10);
    gtk_box_pack_start (GTK_BOX (GTK_DIALOG (st.dlg)->vbox), frame,
			TRUE, TRUE, 0);

    st.vbox = gtk_vbox_new(FALSE, 1);
    gtk_container_add (GTK_CONTAINER (frame), st.vbox);

    /* we only create the channel menu and option menu if there are 
     * more than one channels involved */
    st.channel_menu   = NULL;
    st.channel_option = NULL;

    /* optional portion begins */
    if (bpp != 1)
    {
	table = gtk_table_new (2, 2, FALSE);
	gtk_container_border_width (GTK_CONTAINER (table), 10);
	gtk_box_pack_start (GTK_BOX (st.vbox), table, TRUE, TRUE, 0);

	/* black pullout */
	st.pull = entscale_new(table, 0, 1, "Black pullout (%)",
			       ENTSCALE_INT, &pvals.k_pullout,
			       0.0, 100.0, 1.0, TRUE/*constrain*/,
			       NULL, NULL);
	entscale_set_sensitive(st.pull, (pvals.colourspace == CS_CMYK));

	/* RGB / CMYK / Intensity select */
	label = gtk_label_new("Separate to");
	gtk_table_attach(GTK_TABLE(table), label, 0, 1, 0, 1,
			 GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);
	gtk_widget_show(label);

	hbox = gtk_hbox_new(FALSE, 5);

	toggle = gtk_radio_button_new_with_label(group, "RGB");
	group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle));
	gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0);
	gtk_object_set_user_data(GTK_OBJECT(toggle), &st);
	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle),
				     (pvals.colourspace == CS_RGB));
	gtk_signal_connect (GTK_OBJECT (toggle), "toggled",
			    (GtkSignalFunc) newsprint_cspace_update,
			    GINT_TO_POINTER(CS_RGB));
	gtk_widget_show (toggle);

	toggle = gtk_radio_button_new_with_label (group, "CMYK");
	group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle));
	gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0);
	gtk_object_set_user_data(GTK_OBJECT(toggle), &st);
	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle),
				     (pvals.colourspace == CS_CMYK));
	gtk_signal_connect (GTK_OBJECT (toggle), "toggled",
			    (GtkSignalFunc) newsprint_cspace_update,
			    GINT_TO_POINTER(CS_CMYK));
	gtk_widget_show (toggle);

	toggle = gtk_radio_button_new_with_label (group, "Intensity");
	group = gtk_radio_button_group (GTK_RADIO_BUTTON (toggle));
	gtk_box_pack_start (GTK_BOX (hbox), toggle, TRUE, TRUE, 0);
	gtk_object_set_user_data(GTK_OBJECT(toggle), &st);
	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle),
				     (pvals.colourspace == CS_INTENSITY));
	gtk_signal_connect (GTK_OBJECT (toggle), "toggled",
			    (GtkSignalFunc) newsprint_cspace_update,
			    GINT_TO_POINTER(CS_INTENSITY));
	gtk_widget_show (toggle);

	gtk_table_attach(GTK_TABLE(table), hbox, 1, 2, 0, 1,
			 GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);
	gtk_widget_show(hbox);

	gtk_widget_show(table);

	/* channel lock & factory defaults button */
	align = gtk_alignment_new(0.0, 1.0, 0.1, 1.0);
	hbox = gtk_hbutton_box_new();
	gtk_button_box_set_spacing(GTK_BUTTON_BOX(hbox), 10);
	gtk_button_box_set_layout(GTK_BUTTON_BOX(hbox), GTK_BUTTONBOX_SPREAD);

	gtk_container_add(GTK_CONTAINER(align), hbox);
	gtk_box_pack_start(GTK_BOX(st.vbox), align, TRUE, FALSE, 0);
	/* make sure it went in the right place, since colourspace
	 * radio button callbacks may have already inserted the
	 * channel frames */
	gtk_box_reorder_child(GTK_BOX(st.vbox), align, 1);
	gtk_widget_show(align);
	gtk_widget_show(hbox);

	toggle = gtk_check_button_new_with_label("Lock channels");
	gtk_signal_connect(GTK_OBJECT(toggle), "toggled",
			   (GtkSignalFunc) newsprint_toggle_update,
			   &pvals_ui.lock_channels);
	gtk_object_set_user_data(GTK_OBJECT(toggle), NULL);
	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON (toggle),
				    pvals_ui.lock_channels);
	gtk_box_pack_start(GTK_BOX(hbox), toggle, TRUE, TRUE, 0);
	gtk_widget_show(toggle);

	st.channel_menu   = gtk_menu_new();
	st.channel_option = gtk_option_menu_new();
	gtk_option_menu_set_menu(GTK_OPTION_MENU(st.channel_option),
				 st.channel_menu);
	gtk_widget_show(st.channel_option);
	gtk_box_pack_start(GTK_BOX(hbox), st.channel_option, TRUE, TRUE, 0);

	button = gtk_button_new_with_label("Factory defaults");
	gtk_signal_connect(GTK_OBJECT(button), "clicked",
			   (GtkSignalFunc) newsprint_defaults_callback,
			   &st);
	gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
	gtk_widget_show(button);
    }

    /* Make the channels appropriate for this colourspace and
     * currently selected defaults.  They may have already been
     * created as a result of callbacks to cspace_update from
     * gtk_toggle_button_set_active().  Other channel frames are
     * created lazily the first time they are required. */
    if (!st.chst[pvals.colourspace][0])
    {
	channel_st **chst;

	gen_channels(&st, pvals.colourspace);

	chst = st.chst[pvals.colourspace];

	for(i=0; i < cspace_nchans[pvals.colourspace]; i++)
	    gtk_widget_show(GTK_WIDGET(chst[i]->ch_menuitem));

	/* select the first channel to edit */
	if (st.channel_option)
	    gtk_option_menu_set_history(GTK_OPTION_MENU(st.channel_option),
					chst[0]->ch_menu_num);
	gtk_menu_item_activate(GTK_MENU_ITEM(chst[0]->ch_menuitem));
    }

    gtk_widget_show(st.vbox);
    gtk_widget_show(frame);



    /* anti-alias control */
    frame = gtk_frame_new ("Anti-alias");
    gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN);
    gtk_container_border_width (GTK_CONTAINER (frame), 10);
    gtk_box_pack_start (GTK_BOX (GTK_DIALOG (st.dlg)->vbox), frame,
			TRUE, TRUE, 0);
    
    table = gtk_table_new (1, 2, FALSE);
    gtk_container_border_width (GTK_CONTAINER (table), 10);
    gtk_container_add (GTK_CONTAINER (frame), table);

    entscale_new(table, 1, 0, "oversample ",
		 ENTSCALE_INT, &pvals.oversample,
		 1.0, 15.0, 1.0, TRUE/*constrain*/,
		 NULL, NULL);

    gtk_widget_show (table);
    gtk_widget_show (frame);

    gtk_widget_show (st.dlg);

    gtk_main ();
    gdk_flush ();

    return pint.run;
}

/*  Newsprint interface functions  */

static void
newsprint_close_callback (GtkWidget *widget,
			 gpointer   data)
{
    gtk_main_quit ();
}

static void
newsprint_ok_callback (GtkWidget *widget,
		      gpointer	 data)
{
    pint.run = TRUE;
    gtk_widget_destroy (GTK_WIDGET (data));
}

static void
newsprint_toggle_update (GtkWidget *widget,
			 gpointer   data)
{
    int *toggle_val;

    toggle_val = (int *) data;

    *toggle_val = GTK_TOGGLE_BUTTON(widget)->active;
}


static void
newsprint_cspace_update (GtkWidget *widget,
			 gpointer   data)
{
    NewsprintDialog_st	*st;
    gint		new_cs = GPOINTER_TO_INT(data);
    gint		old_cs = pvals.colourspace;
    channel_st		**chst;
    gint		i;

    st = gtk_object_get_user_data(GTK_OBJECT(widget));

    if (!st)
	printf("newsprint: cspace_update: no state, can't happen!\n");

    if (st)
    {
	/* the CMYK widget looks after the black pullout widget */
	if (new_cs == CS_CMYK)
	{
	    entscale_set_sensitive(st->pull,
				   GTK_TOGGLE_BUTTON(widget)->active);
	}

	/* if we're not activate, then there's nothing more to do */
	if (!GTK_TOGGLE_BUTTON(widget)->active)
	    return;

	pvals.colourspace = new_cs;

	/* make sure we have the necessary channels for the new
         * colourspace */
	if (!st->chst[new_cs][0])
	    gen_channels(st, new_cs);

	/* hide the old channels (if any) */
	if (st->chst[old_cs][0])
	{
	    chst = st->chst[old_cs];

	    for(i=0; i < cspace_nchans[old_cs]; i++)
		gtk_widget_hide(GTK_WIDGET(chst[i]->ch_menuitem));
	}

	/* show the new channels */
	chst = st->chst[new_cs];
	for(i=0; i < cspace_nchans[new_cs]; i++)
	    gtk_widget_show(GTK_WIDGET(chst[i]->ch_menuitem));

	/* and select the first one */
	gtk_option_menu_set_history(GTK_OPTION_MENU(st->channel_option),
				    chst[0]->ch_menu_num);
	gtk_menu_item_activate(GTK_MENU_ITEM(chst[0]->ch_menuitem));
    }
}




/*
 * Newsprint Effect
 */


/*************************************************************/
/* Spot functions */


/* Spot functions define the order in which pixels should be whitened
 * as a cell lightened in colour.  They are defined over the entire
 * cell, and are called over each pixel in the cell.  The cell
 * co-ordinate space ranges from -1.0 .. +1.0 inclusive, in both x- and
 * y-axes.
 *
 * This means the spot function f(x, y) must be defined for:
 *     -1 <= x <= +1, where x is a real number,   and
 *     -1 <= y <= +1, where y is a real number.
 *
 * The function f's range is -1.0 .. +1.0 inclusive, but it is
 * permissible for f to return values outside this range: the nearest
 * valid value will be used instead.  NOTE: this is in contrast with
 * PostScript spot functions, where it is a RangeError for the spot
 * function to go outside these limits.
 *
 * An initially black cell is filled from lowest spot function value
 * to highest.  The actual values returned do not matter - it is their
 * relative orderings that count.  This means that spot functions do
 * not need to be tonally balanced.  A tonally balanced spot function
 * is one which for all slices though the function (eg say at z), the
 * area of the slice = 4z.  In particular, almost all PostScript spot
 * functions are _not_ tonally balanced.
 */


/* The classic growing dot spot function.  This one isn't tonally
 * balanced.  It can be made so, but it's _really_ ugly.  Thanks to
 * Richard Mortier for this one:
 *
 * #define a(r) \
 *     ((r<=1)? G_PI * (r*r) : \
 *	4 * sqrt(r*r - 1)  +  G_PI*r*r  -  4*r*r*acos(1/r))
 *
 *   radius = sqrt(x*x + y*y);
 *
 * return a(radius) / 4; */
static gdouble
spot_round(gdouble x, gdouble y)
{
    return 1 - (x*x + y*y);
}


/* Another commonly seen spot function is the v-shaped wedge. Tonally
 * balanced. */
static gdouble
spot_line(gdouble x, gdouble y)
{
    return ABS(y);
}


/* Square/Diamond dot that never becomes round.  Tonally balanced. */
static gdouble
spot_diamond(gdouble x, gdouble y)
{
    gdouble xy = ABS(x) + ABS(y);
    /* spot only valid for 0 <= xy <= 2 */
    return ((xy <= 1)? 2*xy*xy : 2*xy*xy - 4*(xy-1)*(xy-1)) / 4;
}



/* The following functions were derived from a peice of PostScript by
 * Peter Fink and published in his book, "PostScript Screening: Adobe
 * Accurate Screens" (Adobe Press, 1992).  Adobe Systems Incorporated
 * allow its use, provided the following copyright message is present:
 *
 *  % Film Test Pages for Screenset Development
 *  % Copyright (c) 1991 and 1992 Adobe Systems Incorporated
 *  % All rights reserved.
 *  %
 *  % NOTICE: This code is copyrighted by Adobe Systems Incorporated, and
 *  % may not be reproduced for sale except by permission of Adobe Systems
 *  % Incorporated. Adobe Systems Incorporated grants permission to use
 *  % this code for the development of screen sets for use with Adobe
 *  % Accurate Screens software, as long as the copyright notice remains
 *  % intact.
 *  %
 *  % By Peter Fink 1991/1992
 */

/* Square (or Euclidean) dot.  Also very common. */
static gdouble
spot_PSsquare(gdouble x, gdouble y)
{
    gdouble ax = ABS(x);
    gdouble ay = ABS(y);

    return (ax+ay)>1? ((ay-1)*(ay-1) + (ax-1)*(ax-1)) - 1 : 1-(ay*ay + ax*ax);
}

/* Diamond spot function, again from Peter Fink's PostScript
 * original.  Copyright as for previous function. */
static gdouble
spot_PSdiamond(gdouble x, gdouble y)
{
    gdouble ax = ABS(x);
    gdouble ay = ABS(y);

    return (ax+ay)<=0.75? 1-(ax*ax + ay*ay) :	/* dot */
	( (ax+ay)<=1.23?  1-((ay*0.76) + ax) :	/* to diamond (0.76 distort) */
	  ((ay-1)*(ay-1) + (ax-1)*(ax-1)) -1);	/* back to dot */
}

/* end of Adobe Systems Incorporated copyrighted functions */





/*************************************************************/
/* Spot function to threshold matrix conversion */


/* Each call of the spot function results in one of these */
typedef struct {
    gint	index;	/* (y * width) + x */
    gdouble	value;	/* return value of the spot function */
} order_t;

/* qsort(3) compare function */
static int
order_cmp(const void *va, const void *vb)
{
    const order_t *a = va;
    const order_t *b = vb;

    return (a->value < b->value)? -1 : ((a->value > b->value)? +1 : 0);
}

/* Convert spot function "type" to a threshold matrix of size "width"
 * times "width".  Returns newly allocated threshold matrix.  The
 * reason for qsort()ing the results rather than just using the spot
 * function's value directly as the threshold value is that we want to
 * ensure that the threshold matrix is tonally balanced - that is, for
 * a threshold value of x%, x% of the values in the matrix are < x%.
 *
 * Actually, it turns out that qsort()ing a function which is already
 * balanced can quite significantly detract from the quality of the
 * final result.  This is particularly noticable with the line or
 * diamond spot functions at 45 degrees.  This is because if the spot
 * function has multiple locations with the same value, qsort may use
 * them in any order.  Often, there is quite clearly an optimal order
 * however.  By marking functions as pre-balanced, this random
 * shuffling is avoided.  WARNING: a non-balanced spot function marked
 * as pre-balanced is bad: you'll end up with dark areas becoming too
 * dark or too light, and vice versa for light areas.  This is most
 * easily checked by halftoning an area, then bluring it back - you
 * should get the same colour back again.  The only way of getting a
 * correctly balanced function is by getting a formula for the spot's
 * area as a function of x and y - this can be fairly tough (ie
 * possiblly an integral in two dimensions that must be solved
 * analytically).
 *
 * The threshold matrix is used to compare against image values.  If
 * the image value is greater than the threshold value, then the
 * output pixel is illuminated.  This means that a threshold matrix
 * entry of 0 never causes output pixels to be illuminated.  */
static guchar *
spot2thresh(gint type, gint width)
{
    gdouble	sx, sy;
    gdouble	val;
    spotfn_t	*spotfn;
    guchar	*thresh;
    order_t	*order;
    gint	x, y;
    gint	i;
    gint	wid2 = width*width;
    gint	balanced = spotfn_list[type].balanced;

    thresh = g_new(guchar, wid2);
    spotfn = spotfn_list[type].fn;

    order = g_new(order_t, wid2);

    i = 0;
    for(y=0; y<width; y++)
    {
	for(x=0; x<width; x++)
	{
	    /* scale x & y to -1 ... +1 inclusive */
	    sx = (((gdouble)x) / (width-1) - 0.5) * 2;
	    sy = (((gdouble)y) / (width-1) - 0.5) * 2;
	    val = spotfn(sx, sy);
	    val = BOUNDS(val, -1, 1);	/* interval is inclusive */

	    order[i].index = i;
	    order[i].value = val;
	    i++;
	}
    }

    if (!balanced)
    {
	/* now sort array of (point, value) pairs in ascending order */
	qsort(order, wid2, sizeof(order_t), order_cmp);
    }

    /* compile threshold matrix in order from darkest to lightest */
    for(i=0; i < wid2; i++)
    {
	/* thresh[] contains values from 0 .. 254.  The reason for not
	 * including 255 is so that an image value of 255 remains
	 * unmolested.  It would be bad to filter a completely white
	 * image and end up with black speckles.  */
	if (balanced)
	    thresh[order[i].index] = order[i].value * 0xfe;
	else
	    thresh[order[i].index] = i * 0xff / wid2;
    }

    g_free(order);

    /* TODO: this is where the code to apply a transfer or dot gain
     * function to the threshold matrix would go. */

    return thresh;
}




/**************************************************************/
/* Main loop */


/* This function operates on the image, striding across it in tiles. */
static void
newsprint(GDrawable *drawable)
{
    GPixelRgn	src_rgn, dest_rgn;
    guchar	*src_row, *dest_row;
    guchar	*src, *dest;
    guchar	*thresh[4];
    gdouble	r;
    gdouble	theta;
    gdouble	rot[4];
    gdouble	k_pullout;
    gint	bpp, colour_bpp;
    gint	has_alpha;
    gint	b;
    gint	tile_width;
    gint	width;
    gint	row, col;
    gint	x, y, x_step, y_step;
    gint	x1, y1, x2, y2;
    gint	rx, ry;
    gint	progress, max_progress;
    gint	oversample;
    gint	colourspace;
    gpointer	pr;
    gint	w002;
    TM_INIT();

    width = pvals.cell_width;

    if (width < 0)
	width = -width;
    if (width < 1)
	width = 1;

    oversample = pvals.oversample;
    k_pullout = ((gdouble)pvals.k_pullout) / 100.0;

    width *= oversample;

    tile_width = gimp_tile_width();

    gimp_drawable_mask_bounds(drawable->id, &x1, &y1, &x2, &y2);

    bpp        = gimp_drawable_bpp(drawable->id);
    has_alpha  = gimp_drawable_has_alpha(drawable->id);
    colour_bpp = has_alpha? bpp-1 : bpp;
    colourspace= pvals.colourspace;
    if (bpp == 1) {
	colourspace = CS_GREY;
    } else {
	if (colourspace == CS_GREY)
	    colourspace = CS_RGB;
    }

    /* Bartlett window matrix optimisation */
    w002 = BARTLETT(0,0) * BARTLETT(0,0);
#if 0
    /* It turns out to be slightly slower to cache a pre-computed
     * bartlett matrix!   I put it down to d-cache pollution *shrug* */
    wgt = g_new(gint, oversample*oversample);
    for(y=-oversample/2; y<=oversample/2; y++)
	for(x=-oversample/2; x<=oversample/2; x++)
	    WGT(x,y) = BARTLETT(x,y);
#endif /* 0 */

#define ASRT(_x)						\
do {								\
    if (!VALID_SPOTFN(_x))					\
    {								\
	printf("newsprint: %d is not a valid spot type\n", _x);	\
	_x = SPOTFN_DOT;					\
    }								\
} while(0)

    /* calculate the RGB / CMYK rotations and threshold matrices */
    if (bpp == 1 || colourspace == CS_INTENSITY)
    {
	rot[0]    = DEG2RAD(pvals.gry_ang);	
	thresh[0] = spot2thresh(pvals.gry_spotfn, width);
    }
    else
    {
	gint rf = pvals.red_spotfn;
	gint gf = pvals.grn_spotfn;
	gint bf = pvals.blu_spotfn;

	rot[0] = DEG2RAD(pvals.red_ang);
	rot[1] = DEG2RAD(pvals.grn_ang);
	rot[2] = DEG2RAD(pvals.blu_ang);

	/* always need at least one threshold matrix */
	ASRT(rf);
	spotfn_list[rf].thresh = spot2thresh(rf, width);
	thresh[0] = spotfn_list[rf].thresh;

	/* optimisation: only use separate threshold matrices if the
	 * spot functions are actually different */
	ASRT(gf);
	if (!spotfn_list[gf].thresh)
	    spotfn_list[gf].thresh = spot2thresh(gf, width);
	thresh[1] = spotfn_list[gf].thresh;

	ASRT(bf);
	if (!spotfn_list[bf].thresh)
	    spotfn_list[bf].thresh = spot2thresh(bf, width);
	thresh[2] = spotfn_list[bf].thresh;

	if (colourspace == CS_CMYK)
	{
	    rot[3] = DEG2RAD(pvals.gry_ang);
	    gf = pvals.gry_spotfn;
	    ASRT(gf);
	    if (!spotfn_list[gf].thresh)
		spotfn_list[gf].thresh = spot2thresh(gf, width);	
	    thresh[3] = spotfn_list[gf].thresh;
	}
    }



    /* Initialize progress */
    progress     = 0;
    max_progress = (x2 - x1) * (y2 - y1);

    TM_START();

    for( y = y1; y < y2; y += tile_width - ( y % tile_width ) )
    {
	for( x = x1; x < x2; x += tile_width - ( x % tile_width ) )
	{
	    /* snap to tile boundary */
	    x_step = tile_width - ( x % tile_width );
	    y_step = tile_width - ( y % tile_width );
	    /* don't step off the end of the image */
	    x_step = MIN( x_step, x2-x );
	    y_step = MIN( y_step, y2-y );

	    /* set up the source and dest regions */
	    gimp_pixel_rgn_init (&src_rgn, drawable, x, y, x_step, y_step,
				 FALSE/*dirty*/, FALSE/*shadow*/);

	    gimp_pixel_rgn_init (&dest_rgn, drawable, x, y, x_step, y_step,
				 TRUE/*dirty*/, TRUE/*shadow*/);

	    /* page in the image, one tile at a time */
	    for (pr = gimp_pixel_rgns_register (2, &src_rgn, &dest_rgn);
		 pr != NULL;
		 pr = gimp_pixel_rgns_process (pr))
	    {
		src_row  = src_rgn.data;
		dest_row = dest_rgn.data;

		for (row = 0; row < src_rgn.h; row++)
		{
		    src  = src_row;
		    dest = dest_row;
		    for (col = 0; col < src_rgn.w; col++)
		    {
			guchar data[4];

			rx = (x + col) * oversample;
			ry = (y + row) * oversample;

			/* convert rx and ry to polar (r, theta) */
			r = sqrt(((double)rx)*rx + ((double)ry)*ry);
			theta = atan2(((gdouble)ry), ((gdouble)rx));

			for(b=0; b<colour_bpp; b++)
			    data[b] = src[b];

			/* do colour space conversion */
			switch(colourspace) {
			case CS_CMYK:
			    data[3] = 0xff;

			    data[0] = 0xff - data[0];
			    data[3] = MIN(data[3], data[0]);
			    data[1] = 0xff - data[1];
			    data[3] = MIN(data[3], data[1]);
			    data[2] = 0xff - data[2];
			    data[3] = MIN(data[3], data[2]);

			    data[3] = ((gdouble)data[3]) * k_pullout;

			    data[0] -= data[3];
			    data[1] -= data[3];
			    data[2] -= data[3];
			    break;

			case CS_INTENSITY:
			    data[3] = data[0]; /* save orig for later */
			    data[0] = INTENSITY(data[0], data[1], data[2]);
			    break;

			default:
			    break;
			}
			

			for(b=0; b<cspace_nchans[colourspace]; b++)
			{
			    rx = RINT(r * cos(theta + rot[b]));
			    ry = RINT(r * sin(theta + rot[b]));

			    /* Make sure rx and ry are positive and within
			     * the range 0 .. width-1 (incl).  Can't use %
			     * operator, since its definition on negative
			     * numbers is not helpful.  Can't use ABS(),
			     * since that would cause reflection about the
			     * x- and y-axes.  Relies on integer division
			     * rounding towards zero. */
			    rx -= ((rx - ISNEG(rx)*(width-1)) / width) * width;
			    ry -= ((ry - ISNEG(ry)*(width-1)) / width) * width;

			    {
				guint32 sum = 0;
				gint sx, sy;
				gint tx, ty;
				for(sy=-oversample/2; sy<=oversample/2; sy++)
				    for(sx=-oversample/2; sx<=oversample/2; sx++)
				    {
					tx = rx+sx;
					ty = ry+sy;
					while (tx < 0)  tx += width;
					while (ty < 0)  ty += width;
					while (tx >= width)  tx -= width;
					while (ty >= width)  ty -= width;
					if (data[b] > THRESHn(b, tx, ty))
					    sum += 0xff * BARTLETT(sx, sy);
				    }
				sum /= w002;
				data[b] = sum;
			    }
			}
			if (has_alpha)
			    dest[colour_bpp] = src[colour_bpp];

			/* re-pack the colours into RGB */
			switch(colourspace) {
			case CS_CMYK:
			    data[0] = CLAMPED_ADD(data[0], data[3]);
			    data[1] = CLAMPED_ADD(data[1], data[3]);
			    data[2] = CLAMPED_ADD(data[2], data[3]);
			    data[0] = 0xff - data[0];
			    data[1] = 0xff - data[1];
			    data[2] = 0xff - data[2];
			    break;

			case CS_INTENSITY:
			    if (has_alpha)
			    {
				dest[colour_bpp] = data[0];
				data[0] = 0xff;
			    }
			    data[1] = data[1] * data[0] / 0xff;
			    data[2] = data[2] * data[0] / 0xff;
			    data[0] = data[3] * data[0] / 0xff;
			    break;

			default:
			    /* no other special cases */
			    break;
			}

			for(b=0; b<colour_bpp; b++)
			    dest[b] = data[b];

			src  += src_rgn.bpp;
			dest += dest_rgn.bpp;
		    }
		    src_row  += src_rgn.rowstride;
		    dest_row += dest_rgn.rowstride;
		}

		/* Update progress */
		progress += src_rgn.w * src_rgn.h;
		gimp_progress_update (
		    (double) progress / (double) max_progress);
	    }
	}
    }

    TM_END();

    /* We don't free the threshold matrices, since we're about to
     * exit, and the OS should clean up after us. */

    /* update the affected region */
    gimp_drawable_flush (drawable);
    gimp_drawable_merge_shadow (drawable->id, TRUE);
    gimp_drawable_update (drawable->id, x1, y1, (x2 - x1), (y2 - y1));
}




/**************************************************************/
/* Support routines */

/* Lightly modified entscale:
 *  o Added entscale_set_sensitive() method */


/*************************************************************************/
/**									**/
/**			+++ Entscale					**/
/**									**/
/*************************************************************************/

/* these routines are taken from gflare.c */

/* -*- mode:c -*- */
/*
  Entry and Scale pair (int and double integrated)

  This is an attempt to combine entscale_int and double.
  Never compiled yet.

  TODO:
  - Do the proper thing when the user changes value in entry,
  so that callback should not be called when value is actually not changed.
  - Update delay
 */


static void   entscale_destroy_callback (GtkWidget *widget,
					 gpointer data);
static void   entscale_scale_update (GtkAdjustment *adjustment,
				     gpointer	   data);
static void   entscale_entry_update (GtkWidget *widget,
				     gpointer	data);
/*
 *  Create an entry, a scale and a label, then attach them to
 *  specified table.
 *  1 row and 2 cols of table are needed.
 *
 *  Input:
 *    table:	  table which entscale is attached to
 *    x, y:	  starting row and col in table
 *    caption:	  label string
 *    type:	  type of variable (ENTSCALE_INT or ENTSCALE_DOUBLE)
 *    variable:	  pointer to variable
 *    min, max:	  boundary of scale
 *    step:	  step of scale (ignored when (type == ENTSCALE_INT))
 *    constraint: (bool) true iff the value of *variable should be
 *		  constraint by min and max
 *    callback:	  called when the value is actually changed
 *		  (*variable is automatically changed, so there's no
 *		  need of callback func usually.)
 *    call_data:  data for callback func
 */

Entscale *
entscale_new (GtkWidget *table, gint x, gint y, gchar *caption,
	      EntscaleType type, gpointer variable,
	      gdouble min, gdouble max, gdouble step,
	      gint constraint,
	      EntscaleCallbackFunc callback,
	      gpointer call_data)
{
  Entscale	*entscale;
  GtkWidget	*entry;
  GtkWidget	*scale;
  GtkObject	*adjustment;
  gchar		buffer[256];
  gdouble	val;
  gdouble	constraint_val;

  entscale = g_new0 (Entscale, 1 );
  entscale->type	= type;
  entscale->constraint	= constraint;
  entscale->callback	= callback;
  entscale->call_data	= call_data;

  switch (type)
    {
    case ENTSCALE_INT:
      step = 1.0;
      strcpy (entscale->fmt_string, "%.0f");
      val = *(gint *)variable;
      break;
    case ENTSCALE_DOUBLE:
      sprintf (entscale->fmt_string, "%%.%df", entscale_get_precision (step) + 1);
      val = *(gdouble *)variable;
      break;
    default:
      g_error ("TYPE must be either ENTSCALE_INT or ENTSCALE_DOUBLE");
      val = 0;
      break;
    }

  entscale->label = gtk_label_new (caption);
  gtk_misc_set_alignment (GTK_MISC (entscale->label), 0.0, 0.5);

  /*
    If the first arg of gtk_adjustment_new() isn't between min and
    max, it is automatically corrected by gtk later with
    "value_changed" signal. I don't like this, since I want to leave
    *variable untouched when `constraint' is false.
    The lines below might look oppositely, but this is OK.
   */
  if (constraint)
    constraint_val = val;
  else
    constraint_val = BOUNDS (val, min, max);

  adjustment = entscale->adjustment =
    gtk_adjustment_new (constraint_val, min, max, step, step, 0.0);
  scale = gtk_hscale_new (GTK_ADJUSTMENT (adjustment));
  gtk_widget_set_usize (scale, ENTSCALE_SCALE_WIDTH, 0);
  gtk_scale_set_draw_value (GTK_SCALE (scale), FALSE);

  entry = entscale->entry = gtk_entry_new ();
  gtk_widget_set_usize (entry, ENTSCALE_ENTRY_WIDTH, 0);
  sprintf (buffer, entscale->fmt_string, val);
  gtk_entry_set_text (GTK_ENTRY (entry), buffer);

  /* entscale is done */
  gtk_object_set_user_data (GTK_OBJECT(adjustment), entscale);
  gtk_object_set_user_data (GTK_OBJECT(entry), entscale);

  /* now ready for signals */
  gtk_signal_connect (GTK_OBJECT (entry), "changed",
		      (GtkSignalFunc) entscale_entry_update,
		      variable);
  gtk_signal_connect (GTK_OBJECT (adjustment), "value_changed",
		      (GtkSignalFunc) entscale_scale_update,
		      variable);
  gtk_signal_connect (GTK_OBJECT (entry), "destroy",
		      (GtkSignalFunc) entscale_destroy_callback,
		      NULL );

  /* start packing */
  entscale->hbox = gtk_hbox_new (FALSE, 5);
  gtk_box_pack_start (GTK_BOX (entscale->hbox), scale, TRUE, TRUE, 0);
  gtk_box_pack_start (GTK_BOX (entscale->hbox), entry, FALSE, TRUE, 0);

  gtk_table_attach (GTK_TABLE (table), entscale->label, x, x+1, y, y+1,
		    GTK_FILL, GTK_FILL, 0, 0);
  gtk_table_attach (GTK_TABLE (table), entscale->hbox, x+1, x+2, y, y+1,
		    GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);

  gtk_widget_show (entscale->label);
  gtk_widget_show (entry);
  gtk_widget_show (scale);
  gtk_widget_show (entscale->hbox);

  return entscale;
}

static int
entscale_get_precision (gdouble step)
{
  int precision;
  if (step <= 0)
    return 0;
  for (precision = 0; pow (0.1, precision) > step; precision++) ;
  return precision;
}

static void
entscale_destroy_callback (GtkWidget *widget,
			   gpointer data)
{
  Entscale *entscale;

  entscale = gtk_object_get_user_data (GTK_OBJECT (widget));
  g_free (entscale);
}
static void
entscale_scale_update (GtkAdjustment *adjustment,
		       gpointer	     data)
{
  Entscale	*entscale;
  GtkEntry	*entry;
  gchar		buffer[256];
  gdouble	old_val, new_val;

  entscale = gtk_object_get_user_data (GTK_OBJECT (adjustment));

  new_val = adjustment->value;
  /* adjustmet->value is always constrainted */

  switch (entscale->type)
    {
    case ENTSCALE_INT:
      old_val = *(gint*) data;
      *(gint*) data = (gint) new_val;
      DEBUG_PRINT (("entscale_scale_update(int): fmt=\"%s\" old=%g new=%g\n",
		    entscale->fmt_string, old_val, new_val));
      break;
    case ENTSCALE_DOUBLE:
      old_val = *(gdouble*) data;
      *(gdouble*) data = new_val;
      DEBUG_PRINT (("entscale_scale_update(double): fmt=\"%s\" old=%g new=%g\n",
		    entscale->fmt_string, old_val, new_val));
      break;
    default:
      g_warning ("entscale_scale_update: invalid type");
      return;
    }

  entry = GTK_ENTRY (entscale->entry);
  sprintf (buffer, entscale->fmt_string, new_val);

  /* avoid infinite loop (scale, entry, scale, entry ...) */
  gtk_signal_handler_block_by_data (GTK_OBJECT(entry), data);
  gtk_entry_set_text (entry, buffer);
  gtk_signal_handler_unblock_by_data (GTK_OBJECT(entry), data);

  if (entscale->callback && (old_val != new_val))
    (*entscale->callback) (new_val, entscale->call_data);
}

static void
entscale_entry_update (GtkWidget *widget,
		       gpointer	  data)
{
  Entscale	*entscale;
  GtkAdjustment *adjustment;
  gdouble	old_val, val, new_val, constraint_val;

  entscale = gtk_object_get_user_data (GTK_OBJECT (widget));
  adjustment = GTK_ADJUSTMENT (entscale->adjustment);

  val = atof (gtk_entry_get_text (GTK_ENTRY (widget)));

  constraint_val = BOUNDS (val, adjustment->lower, adjustment->upper);

  if (entscale->constraint)
    new_val = constraint_val;
  else
    new_val = val;

  switch (entscale->type)
    {
    case ENTSCALE_INT:
      old_val = *(gint*) data;
      *(gint*) data = (gint) new_val;
      DEBUG_PRINT (("entscale_entry_update(int): old=%g new=%g const=%g\n",
		    old_val, new_val, constraint_val));
      break;
    case ENTSCALE_DOUBLE:
      old_val = *(gdouble*) data;
      *(gdouble*) data = new_val;
      DEBUG_PRINT (("entscale_entry_update(double): old=%g new=%g const=%g\n",
		    old_val, new_val, constraint_val));
      break;
    default:
      g_warning ("entscale_entry_update: invalid type");
      return;
    }

  adjustment->value = constraint_val;
  /* avoid infinite loop (scale, entry, scale, entry ...) */
  gtk_signal_handler_block_by_data (GTK_OBJECT(adjustment), data );
  gtk_signal_emit_by_name (GTK_OBJECT(adjustment), "value_changed");
  gtk_signal_handler_unblock_by_data (GTK_OBJECT(adjustment), data );

  if (entscale->callback && (old_val != new_val))
    (*entscale->callback) (new_val, entscale->call_data);
}


static void entscale_set_sensitive(Entscale *ent, gint sensitive)
{
    gtk_widget_set_sensitive(GTK_WIDGET(ent->label), sensitive);
    gtk_widget_set_sensitive(GTK_WIDGET(ent->hbox), sensitive);
}


static void entscale_set_value(Entscale *ent, gfloat value)
{
    gtk_adjustment_set_value(GTK_ADJUSTMENT(ent->adjustment), value);
}
