/*
 * Copyright (C) 2005-2017 the xine-project
 *
 * 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 *
 * windows, generated from XML and using JS and gxine's .gtkrc
 */

#include "globals.h"

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <pthread.h>

#include <gdk/gdk.h>
#include <gtk/gtk.h>

#include <xine/xmlparser.h>

#include "xml_widgets.h"
#include "drag_drop.h"
#include "gtkflipbutton.h"
#include "ui.h"
#include "utils.h"
#include "engine.h"

#define TN_(txt) (txt)

#ifdef MAKE_TRANSLATION_SOURCE
# define TRANSLATE(txt) (txt)

static void
comment (const xml_node_t *node, const char *text)
{
  char *lf;

  g_print (_("\n// node type: %s\n"), node->name);
  if (!text)
    return;

  while ((lf = strchr (text, '\n')) != NULL)
  {
    *lf = 0;
    printf ("// %s\n", text);
    text = lf + 1;
  }
  if (*text)
    printf ("// %s\n", text);
}

static void
translatable (const char *text)
{
  if (!text || !*text)
    return;

  assert (*text == '~');
  if (!*++text)
    return;

  printf ("TN_(\"");
  --text;
  while (*++text)
  {
    switch (*text)
    {
    case '"':
      printf ("\\\"");
      break;
    case '\n':
      printf ("\\n\"\n\"");
      break;
    default:
      putchar (*text);
    }
  }
  puts ("\");");
}

#define xml_error(msg, ...) g_printerr ((msg), ## __VA_ARGS__)

#else
# define TRANSLATE(txt) my_gettext (txt)

struct widget_ref {
  GtkWidget *object;
  char id[0];
};

static pthread_mutex_t xml_lock = PTHREAD_MUTEX_INITIALIZER;
static se_o_t *se_widget_space = NULL;
static GSList *mnemonic_list, *widget_id_list;
static gchar  *xml_domain;

static const char *
my_gettext (const char *src)
{
  if (!src)
    return "";
  if (*src != '~')
    return src;
  src = dgettext (xml_domain ? xml_domain : PACKAGE".theme", src + 1);
  return src + (*src == '~');
}

static gboolean
check_stock (const char *stock)
{
  GtkStockItem item;
  return gtk_stock_lookup (stock, &item)
	 || gtk_icon_factory_lookup_default (stock);
}

/* yes, this is intended to be annoying */
#define xml_error(msg, ...) \
  display_error_modal (FROM_GXINE, _("XML error"), (msg), ##__VA_ARGS__)

static struct widget_ref *widget_ref_new (GtkWidget *object, const char *id)
{
  struct widget_ref *ref = malloc (sizeof (*ref) + strlen (id) + 1);
  ref->object = object;
  strcpy (ref->id, id);
  return ref;
}
#endif

/* Callbacks */

#ifndef MAKE_TRANSLATION_SOURCE
static void
destroy_notify (gpointer data)
{
  free (data);
}

static gboolean
button_js_cb (GtkWidget *w, const char *data)
{
  if (data && !no_recursion)
    engine_exec (data + *data + 2, NULL, NULL, data + 1);
  return TRUE;
}

static gboolean
button_toggled_cb (GtkToggleButton *w, gpointer data)
{
  return
    button_js_cb
      (NULL, g_object_get_data (G_OBJECT (w),
                                gtk_toggle_button_get_active(w) ? "js-activate" : "js-deactivate"));
}

static void
stock_control_cb (GtkWidget *w, gpointer data)
{
  if (no_recursion)
    return;
  engine_exec (data, NULL, NULL, NULL);
  /* avoid problems with the space key (key bindings) */
  gtk_window_set_focus (GTK_WINDOW (gtk_widget_get_toplevel (w)), NULL);
}

static void
stock_toggle_cb (GtkToggleButton *button, gpointer data)
{
  if (!no_recursion)
    ui_set_status (UI_CURRENT_STATE);
}

#define JS_WIDGET() ((se_o_t *)JS_GetPrivate (cx, JS_THIS_OBJECT (cx, vp)))->user_data

static JSBool
js_set_show (JSContext *cx, uintN argc, jsval *vp)
{
  int v, all = 0;
  jsval *argv = JS_ARGV (cx, vp);
  se_log_fncall ("<widget>.set_show");
  se_argc_check_range (1, 2, "<widget>.set_show");
  se_arg_is_int_or_bool (0, "<widget>.set_show");
  JS_ValueToBoolean(cx, argv[0], &v);
  if (argc == 2)
    JS_ValueToBoolean(cx, argv[0], &all);
  if (all)
  {
    if (v)
      gtk_widget_show_all (JS_WIDGET ());
    else
      gtk_widget_hide (JS_WIDGET ());
  }
  else if (v)
    gtk_widget_show (JS_WIDGET ());
  else
    gtk_widget_hide (JS_WIDGET ());
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool
js_set_sensitive (JSContext *cx, uintN argc, jsval *vp)
{
  int v;
  jsval *argv = JS_ARGV (cx, vp);
  se_log_fncall ("<widget>.set_sensitive");
  se_argc_check (1, "<widget>.set_sensitive");
  se_arg_is_int_or_bool (0, "<widget>.set_sensitive");
  JS_ValueToBoolean (cx, argv[0], &v);
  gtk_widget_set_sensitive (JS_WIDGET (), v);
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool
js_activate (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall ("<widget>.activate");
  gtk_widget_activate (JS_WIDGET ());
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static gboolean
js_listen_cb (gpointer widget)
{
  engine_exec (g_object_get_data (widget, "js-listen"),
	       NULL, NULL, _("XML widget listener"));
  return FALSE;
}

static int
js_listen (void *widget, se_t *cx, se_o_t *obj, se_prop_t *prop,
	   se_prop_read_t r)
{
  g_idle_add (js_listen_cb, widget);
  return 0;
}
#endif

/* Misc */

#ifndef MAKE_TRANSLATION_SOURCE

#define xml_parser_get_bool(node,prop) \
  xml_parser_get_property_int ((node), (prop), FALSE)

static void
command_attach (GtkWidget *w, const char *event, const char *label,
		const char *cmd, const char *what)
{
  GObject *object = G_OBJECT (w);
  char *data = g_strdup_printf ("%c%s%c%s", (int) strlen (what), what, 0, cmd);
  g_object_set_data_full (object, label, data, destroy_notify);
  if (event)
    g_signal_connect (object, event, G_CALLBACK (button_js_cb), data);
}
#endif

/* XML parser */

static GtkWidget *widget_parse (xml_node_t *);

#ifndef MAKE_TRANSLATION_SOURCE
static int
lookup_type (xml_node_t *node, const char *attr, const char *const labels[])
{
  const char *arg = xml_parser_get_property (node, attr) ? : "";
  int type = -1;
  while (labels[++type])
    if (!strcasecmp (arg, labels[type]))
      break;
  return type;
}

static int
lookup_shadow_type (xml_node_t *node)
{
  static const char *const labels[] = {
    "etched-out", "etched-in", "out", "in", NULL
  };
  return G_N_ELEMENTS (labels) - 1 - lookup_type (node, "shadow", labels);
}

static int
lookup_image_size (xml_node_t *node)
{
  static const char *const labels[] = {
    "menu", "small-toolbar", "large-toolbar", "dnd", "drag-drop",
    "dialog", "dialogue", NULL
  };
  static const guchar map[G_N_ELEMENTS (labels)] = {
    GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_SMALL_TOOLBAR,
    GTK_ICON_SIZE_LARGE_TOOLBAR, GTK_ICON_SIZE_DND, GTK_ICON_SIZE_DND,
    GTK_ICON_SIZE_DIALOG, GTK_ICON_SIZE_DIALOG,
    GTK_ICON_SIZE_BUTTON /* default */
  };

  return map[lookup_type (node, "size", labels)];
}
#endif

#ifndef MAKE_TRANSLATION_SOURCE
static int
parse_attach (xml_node_t *node, const char *attr, int dflt)
{
  int value = 0;
  const char *attach = xml_parser_get_property (node, attr);
  if (!attach)
    return dflt;
  while (*attach)
  {
    int or = 0;
    if (!strncasecmp (attach, "expand", 6))
    {
      or = GTK_EXPAND;
      attach += 6;
    }
    else if (!strncasecmp (attach, "fill", 4))
    {
      or = GTK_FILL;
      attach += 4;
    }
    else if (!strncasecmp (attach, "shrink", 6))
    {
      or = GTK_SHRINK;
      attach += 6;
    }
    if (*attach && *attach++ != ',')
      break;
    value |= or;
  }
  return value;
}
#endif

/* Common properties:
 *	NAME		TYPE	DEFAULT	COMMENT
 *	name		string	varies	for gtkrc styling
 *	width		int	varies	widget's request width
 *	height		int	varies	widget's request height
 *	show		string	none	command(s) on widget being auto-shown
 *	hide		string	none	command(s) on widget being auto-hidden
 */

/* Declarations */
static GtkWidget *create_box (xml_node_t *, gboolean);


/*
 * Interactive widgets
 */

#ifndef MAKE_TRANSLATION_SOURCE
static GtkWidget *
gxine_toggle_button_new_from_stock (const char *stock)
{
  GtkWidget *btn = gtk_toggle_button_new_with_label (stock);
  gtk_button_set_use_stock (GTK_BUTTON (btn), TRUE);
  return btn;
}
#endif

static GtkWidget *
create_button (xml_node_t *node, gboolean click)
/* Properties:
 *	stock		string	none		stock item
 *	label		string	none		text if stock is not set [translatable]
 *	image		string	none		stock image if stock is not set
 *	alt		string	none		alternate text [translatable]
 *	relief		string	"normal"	(or "half" or "none")
 *	mnemonic	boolean	false		underline usage
 *	onclick		string	none		Javascript, executed on click
 *
 * If none of stock, label, image are used, child items are expected.
 * You get an automatic centred hbox to contain them.
 */
{
  const char *label, *image, *alt;
#ifndef MAKE_TRANSLATION_SOURCE
  GtkWidget *button;
  static const char * const relief[] = { "none", "half", NULL };

  typedef GtkWidget *(*button_new_t) (const char *);
  typedef GtkWidget *(*plain_new_t) (void);
  static const struct {
    button_new_t stock[2], mnemonic[2], label[2];
    plain_new_t plain[2];
  } newbtn = {
    { gxine_toggle_button_new_from_stock, gtk_button_new_from_stock },
    { gtk_toggle_button_new_with_mnemonic, gtk_button_new_with_mnemonic },
    { gtk_toggle_button_new_with_label, gtk_button_new_with_label },
    { gtk_toggle_button_new, gtk_button_new },
  };
#endif

  alt = xml_parser_get_property (node, "alt");

  if ((image = xml_parser_get_property (node, "stock")) != NULL)
  {
#ifdef MAKE_TRANSLATION_SOURCE
    if (alt && *alt == '~')
    {
      const char *prop = xml_parser_get_property (node, "onclick");
      comment (node, prop ? prop : image);
      translatable (alt);
    }
    return NULL;
#else
    if (check_stock (image))
      button = newbtn.stock[click] (image);
    else if (alt)
      button = xml_parser_get_bool (node, "mnemonic")
	       ? newbtn.mnemonic[click] (TRANSLATE (alt))
	       : newbtn.label[click] (TRANSLATE (alt));
    else
    {
      button = newbtn.plain[click] ();
	g_object_set (G_OBJECT (button), "image",
		      check_stock (image)
		      ? gtk_image_new_from_stock (image, GTK_ICON_SIZE_BUTTON)
		      : gtk_image_new_from_icon_name (image, GTK_ICON_SIZE_BUTTON),
		      NULL);
    }

    goto created;
#endif
  }

  image = xml_parser_get_property (node, "image");
  label = xml_parser_get_property (node, "label");
#ifdef MAKE_TRANSLATION_SOURCE
  if (label && *label == '~')
  {
    comment (node, xml_parser_get_property (node, "onclick"));
    translatable (label);
  }
  else if (image && alt && *alt == '~')
  {
    const char *prop = xml_parser_get_property (node, "onclick");
    comment (node, prop ? prop : image);
    translatable (alt);
  }
  else if (!label && !image) /* other content */
    create_box (node, FALSE);
  return NULL;
#else

  if (label && !image)
  {
    button = xml_parser_get_bool (node, "mnemonic")
	     ? newbtn.mnemonic[click] (TRANSLATE (label))
	     : newbtn.label[click] (TRANSLATE (label));
    goto created;
  }

  button = newbtn.plain[click] ();
  if (label || image)
  {
    if (label)
      g_object_set (G_OBJECT (button),
                    "label", TRANSLATE (label),
                    "use-underline", xml_parser_get_bool (node, "mnemonic"),
                    NULL);

    if (image && check_stock (image))
      /* image stock item exists */
      g_object_set (G_OBJECT (button),
	"image", gtk_image_new_from_stock (image, GTK_ICON_SIZE_BUTTON),
	NULL);
    else if (!label && alt)
      /* image stock item doesn't exist but we have alt text */
      g_object_set (G_OBJECT (button),
	"label", TRANSLATE (alt),
	NULL);
    else if (!label && image)
      /* no alt text; try themed icon name */
      g_object_set (G_OBJECT (button),
	"image", gtk_image_new_from_icon_name (image, GTK_ICON_SIZE_BUTTON),
	NULL);
    else if (!label)
      g_object_set (G_OBJECT (button),
	"image", gtk_image_new_from_stock (GTK_STOCK_MISSING_IMAGE,
					   GTK_ICON_SIZE_BUTTON),
	NULL);
  }
  else
  {
    /* no label, no image: must be other content */
    GtkWidget *align = gtk_alignment_new (0.5, 0.5, 0, 0);
    gtk_container_add (GTK_CONTAINER (button), align);
    /* create & attach the child element (implied hbox) */
    gtk_container_add (GTK_CONTAINER (align), create_box (node, FALSE));
  }

  created:
  gtk_button_set_relief (GTK_BUTTON (button),
			 2 - lookup_type (node, "relief", relief));
  if (click)
  {
    /* set the command which is executed when the button is clicked */
    label = xml_parser_get_property (node, "onclick");
    if (label)
      command_attach (button, "clicked", "js-onclick", label,
		      _("XML button click"));
  }
  return button;
#endif /* ! MAKE_TRANSLATION_SOURCE */
}


/*
static GtkWidget *
create_flip_button (xml_node_t *node)
*/
/* Properties:
 *	activate	string	none	Javascript, executed on click (->on)
 *	deactivate	string	none	Javascript, executed on click (->off)
 * Contains exactly two children.
 */
/*
{
  GtkWidget *button, *off = NULL, *on = NULL;
  char *cmd;

  if (node->child)
  {
    off = node->child ? widget_parse (node->child) : NULL;
    if (node->child->next)
      on = widget_parse (node->child);
  }
  button = gtk_flip_button_new (off, on);

  if ((cmd = xml_parser_get_property (node, "activate")) != NULL)
    g_object_set_data_full (G_OBJECT (button), "js-activate", g_strdup (cmd),
			    destroy_notify);
  if ((cmd = xml_parser_get_property (node, "activate")) != NULL)
    g_object_set_data_full (G_OBJECT (button), "js-deactivate", g_strdup (cmd),
			    destroy_notify);
  g_signal_connect (G_OBJECT (button), "toggled",
		    G_CALLBACK (button_toggled_cb), NULL);

  return button;
}
*/

#ifndef MAKE_TRANSLATION_SOURCE
typedef enum { BUTTON = 1, FLIP, SLIDER, SPIN } stock_e;
typedef struct {
  stock_e type;
  int control;
  const char *name, *tip, *id1, *id2, *cmd1, *cmd2;
} stock_t;

static GtkWidget *
stock_button_common (GtkWidget *button, int control)
{
  ui_register_control_button (control, button);
  gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
  gtk_widget_show_all (button);
  return button;
}

static void
set_tip (GtkWidget *w, const stock_t *stock)
{
  if (stock->tip)
    gtk_widget_set_tooltip_text (w, TRANSLATE (*stock->tip ? stock->tip : stock->name));
}

static gboolean
slider_button_cb (GtkWidget *widget, GdkEventButton *ev, gpointer data)
{
  gtk_grab_remove (widget);
  gdk_keyboard_ungrab (ev->time);
  gdk_pointer_ungrab (ev->time);
  gtk_widget_hide (widget);
  gtk_toggle_button_set_active (data, FALSE);
  return FALSE;
}

static gboolean
slider_widget_button_cb (GtkWidget *widget, GdkEventButton *ev, gpointer data)
{
  return slider_button_cb (gtk_widget_get_toplevel (widget), ev, data);
}

#include <gdk/gdkkeysyms.h>
#if GTK_CHECK_VERSION(3, 0, 0)
#include <gdk/gdkkeysyms-compat.h>
#endif

static gboolean
slider_key_cb (GtkWidget *window, GdkEventKey *ev, gpointer data)
{
  switch (ev->keyval)
  {
  case GDK_Escape:
  case GDK_KP_Enter:
  case GDK_Return:
  case GDK_space:
    return slider_button_cb (window, (GdkEventButton *) ev, data);
  default:
    return FALSE;
  }
}

static gboolean
slider_window_cb (GtkWidget *button, gpointer window)
{
  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
    return FALSE;

  gtk_window_set_transient_for (window,
				GTK_WINDOW (gtk_widget_get_toplevel (button)));

  GdkWindow *parent = gtk_widget_get_parent_window (button);
  GtkAllocation ppos;
  gdk_window_get_position (parent, &ppos.x, &ppos.y);
#if !GTK_CHECK_VERSION(2, 24, 0)
  gdk_drawable_get_size (GDK_DRAWABLE (parent), &ppos.width, &ppos.height);
#else
  ppos.width = gdk_window_get_width (parent);
  ppos.height = gdk_window_get_height (parent);
#endif

  GdkScreen *screen = gtk_widget_get_screen (button);
  gint monitor = gdk_screen_get_monitor_at_window (screen, parent);
  GdkRectangle geom;
  gdk_screen_get_monitor_geometry (screen, monitor, &geom);

  gint wx, wy;
  GtkRequisition req;
  gtk_widget_size_request (window, &req);
  gtk_window_get_default_size (window, &wx, &wy);
  if (wx > req.width)
    req.width = wx;
  if (wy > req.height)
    req.height = wy;

  GtkAllocation allocation = { 0 };
  gtk_widget_get_allocation (button, &allocation);
  wx = allocation.x + ppos.x; /* button left */
  wy = allocation.y + ppos.y; /* button top */

  if (GTK_IS_VSCALE (gtk_bin_get_child (GTK_BIN (window))))
  {
    /* arrange vertically, preferably below */
    wx += (allocation.width - req.width) / 2; /* centre-aligned */
    wy += (wy + req.height + allocation.height > geom.y + geom.height)
	? -req.height			/* below (if enough space) */
        : allocation.height;            /* above */
  }
  else if (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL)
  {
    /* arrange vertically, preferably to the left (RTL) */
    wx -= (wx - req.width < geom.x)
        ? -allocation.width             /* left (if enough space) */
	: req.width;			/* right */
    wy += (allocation.height - req.height) / 2; /* centre-aligned */
  }
  else
  {
    /* arrange vertically, preferably to the right (LTR) */
    wx += (wx + req.width + allocation.width > geom.x + geom.width)
	? -req.width			/* right (if enough space) */
        : allocation.width;             /* left */
    wy += (allocation.height - req.height) / 2; /* centre-aligned */
  }

  gtk_window_move (GTK_WINDOW (window), wx, wy);
  gtk_widget_show_all (window);

  GdkWindow *gdk_window = gtk_widget_get_window (GTK_WIDGET (window));
  gdk_keyboard_grab (gdk_window, TRUE, GDK_CURRENT_TIME);
  gdk_pointer_grab (gdk_window, TRUE,
		    GDK_BUTTON_RELEASE_MASK, NULL, NULL, GDK_CURRENT_TIME);
  gtk_grab_add (window);

  return TRUE;
}
#endif

static GtkWidget *
create_stock_gxine (xml_node_t *node)
/* Properties:
 *	type		string	none	widget identifer (see stock[].name)
 *	vertical	bool	FALSE	slider orientation
 *	inverted	bool	FALSE	slider direction reversal
 *	show-value	bool	FALSE	show slider value numerically
 *	popup		bool	FALSE	slider is a popup
 *					(use button properties except onclick)
 *	window-width	int	-1	popup window width
 *	window-height	int	-1	popup window height
 */
{
#ifndef MAKE_TRANSLATION_SOURCE
  static const stock_t stock[] = {
    { BUTTON,	Control_PLAY,		"play",		TN_("Start or resume a stream\n(at normal speed)"),
      GTK_STOCK_MEDIA_PLAY, NULL,			"play ();" },
    { BUTTON,	Control_FASTFWD,	"forward",	TN_("Fast forward"),
      GTK_STOCK_MEDIA_FORWARD, NULL,			"if (!is_live_stream ()) av_speed.v = 5;" },
    { BUTTON,	Control_PAUSE,		"pause",	TN_("Pause"),
      GTK_STOCK_MEDIA_PAUSE, NULL,			"if (!is_live_stream ()) pause ();" },
    { BUTTON,	Control_STOP,		"soft-stop",	TN_("Stop"),
      GTK_STOCK_MEDIA_STOP, NULL,			"vdr ('STOP') || stop ();" },
    { BUTTON,	Control_STOP,		"stop",		TN_("Stop stream playback"),
      GTK_STOCK_MEDIA_STOP, NULL,			"stop ();" },
    { FLIP,	Control_MUTE,		"mute",		TN_("Mute/unmute"),
      GXINE_MEDIA_SPEAKER_MUTE, GXINE_MEDIA_SPEAKER,	"if (!vdr ('MUTE')) ao_mute.v = false;", "if (!vdr ('MUTE')) ao_mute.v = true;" },
    { SLIDER,	Control_SEEKER,		"stream",	TN_("Stream position") },
    { SPIN,	Control_AUDIO_CHANNEL,	TN_("Audio channel"), "" },
    { SPIN,	Control_SPU_CHANNEL,	TN_("Subtitle channel"), "" },
    { SLIDER,	Control_COMPRESSOR,	TN_("Compressor"), "" },
    { SLIDER,	Control_AMPLIFIER,	TN_("Amplifier"), "" },
    { SLIDER,	Control_AV_SYNC,	TN_("A/V sync"), "" },
    { SLIDER,	Control_SPU_SYNC,	TN_("SPU sync"), "" },
    { SLIDER,	Control_HUE,		TN_("Hue"), "" },
    { SLIDER,	Control_SATURATION,	TN_("Saturation"), "" },
    { SLIDER,	Control_CONTRAST,	TN_("Contrast"), "" },
    { SLIDER,	Control_BRIGHTNESS,	TN_("Brightness"), "" },
#ifdef XINE_PARAM_VO_GAMMA
    { SLIDER,	Control_GAMMA,		TN_("Gamma"), "" },
#endif
#ifdef XINE_PARAM_VO_SHARPNESS
    { SLIDER,	Control_SHARPNESS,	TN_("Sharpness"), "" },
    { SLIDER,	Control_NOISE_REDUCE,	TN_("Noise reduction"), "" },
#endif
    { SLIDER,	Control_VOLUME,		TN_("Volume"), "" },
    { SLIDER,	Control_EQ_30,		"eq-30",	TN_("Graphic equaliser, 30Hz") },
    { SLIDER,	Control_EQ_60,		"eq-60",	TN_("Graphic equaliser, 60Hz") },
    { SLIDER,	Control_EQ_125,		"eq-125",	TN_("Graphic equaliser, 125Hz") },
    { SLIDER,	Control_EQ_250,		"eq-250",	TN_("Graphic equaliser, 250Hz") },
    { SLIDER,	Control_EQ_500,		"eq-500",	TN_("Graphic equaliser, 500Hz") },
    { SLIDER,	Control_EQ_1K,		"eq-1k",	TN_("Graphic equaliser, 1kHz") },
    { SLIDER,	Control_EQ_2K,		"eq-2k",	TN_("Graphic equaliser, 2kHz") },
    { SLIDER,	Control_EQ_4K,		"eq-4k",	TN_("Graphic equaliser, 4kHz") },
    { SLIDER,	Control_EQ_8K,		"eq-8k",	TN_("Graphic equaliser, 8kHz") },
    { SLIDER,	Control_EQ_16K,		"eq-16k",	TN_("Graphic equaliser, 16kHz") },
  };
  static const char *const spintypes[] = { "slider", "hslider", "vslider", "spin", NULL };

  GtkWidget *w;
  size_t i;
  const char *id = xml_parser_get_property (node, "type");

  if (!id)
    return NULL;

  for (i = 0; i < sizeof(stock)/sizeof(stock[0]); ++i)
  {
    enum { CTRL_SLIDER, CTRL_SLIDER_H, CTRL_SLIDER_V, CTRL_SPIN, CTRL_ANY } spin, spec;

    if (strcasecmp (id, stock[i].name))
      continue;
    switch (stock[i].type)
    {
    case BUTTON:
      w = check_stock (stock[i].id1)
	  ? ui_toggle_button_new_stock (stock[i].id1)
	  : ui_toggle_button_new_icon_name (stock[i].id1);
      g_object_connect (G_OBJECT (w),
	"signal::clicked", G_CALLBACK (stock_control_cb), (char *)stock[i].cmd1,
	"signal::toggled", G_CALLBACK (stock_toggle_cb), NULL,
	NULL);
      set_tip (w, &stock[i]);
      return stock_button_common (w, stock[i].control);

    case FLIP:
      w = check_stock (stock[i].id1)
	  ? gtk_flip_button_new_from_stock (stock[i].id1, stock[i].id2,
					    GTK_ICON_SIZE_BUTTON)
	  : gtk_flip_button_new_from_icon_names (stock[i].id1, stock[i].id2,
						 GTK_ICON_SIZE_BUTTON);
      command_attach (w, NULL, "js-activate", stock[i].cmd1, _("XML button click"));
      command_attach (w, NULL, "js-deactivate", stock[i].cmd2, _("XML button click"));
      g_signal_connect (G_OBJECT (w), "toggled",
			G_CALLBACK (button_toggled_cb), NULL);
      set_tip (w, &stock[i]);
      return stock_button_common (w, stock[i].control);

    case SLIDER:
      spin = CTRL_SLIDER;
      goto do_spin_slider;

    case SPIN:
      spin = CTRL_SPIN;

      do_spin_slider:
      spec = (typeof (spec)) lookup_type (node, "mode", spintypes);

      do_spin_slider_again:
      switch (spec)
      {
      case CTRL_SLIDER:
	w = xml_parser_get_bool (node, "vertical")
	    ? gtk_vscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
						(stock[i].control)))
	    : gtk_hscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
						(stock[i].control)));
	break;
      case CTRL_SLIDER_H:
	w = gtk_hscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
					      (stock[i].control)));
	break;
      case CTRL_SLIDER_V:
	w = gtk_vscale_new (GTK_ADJUSTMENT (ui_register_control_adjustment
					      (stock[i].control)));
	break;
      case CTRL_SPIN:
	w = gtk_spin_button_new
	      (GTK_ADJUSTMENT (ui_register_control_adjustment
				 (stock[i].control)),
	     1.0, 0);
	break;
      default:
	spec = spin;
	goto do_spin_slider_again;
      }

      ui_register_adjustment_widget (stock[i].control, w);

      if (GTK_IS_SCALE (w))
      {
	gtk_scale_set_digits (GTK_SCALE (w), 0);
	gtk_scale_set_draw_value
	  (GTK_SCALE (w), xml_parser_get_bool (node, "show-value"));
	gtk_range_set_inverted
	  (GTK_RANGE (w), xml_parser_get_bool (node, "inverted"));
	if (xml_parser_get_bool (node, "popup"))
	{
	  GtkWindow *window = GTK_WINDOW (gtk_window_new (GTK_WINDOW_POPUP));
	  gtk_window_set_default_size (window,
		xml_parser_get_property_int (node, "window-width", -1),
		xml_parser_get_property_int (node, "window-height", -1));
	  gtk_window_set_decorated (window, FALSE);
	  gtk_window_set_skip_pager_hint (window, TRUE);
	  gtk_window_set_skip_taskbar_hint (window, TRUE);
	  gtk_container_add (GTK_CONTAINER (window), w);
	  gtk_widget_add_events ((GtkWidget *)window, GDK_BUTTON_RELEASE_MASK);
	  gtk_widget_show (w);
	  GtkWidget *btn = create_button (node, FALSE);
	  g_object_connect (G_OBJECT (window),
	    "signal::destroy-event", gtk_widget_hide_on_delete, btn,
	    "signal::button-release-event", G_CALLBACK (slider_button_cb), btn,
	    "signal::key-press-event", G_CALLBACK (slider_key_cb), btn,
	    NULL);
	  g_signal_connect (G_OBJECT (w), "button-release-event",
			    G_CALLBACK (slider_widget_button_cb), btn);
	  g_signal_connect (G_OBJECT (btn), "toggled",
			    G_CALLBACK (slider_window_cb), window);
	  w = btn; /* for popups, we return the button, not the slider */
	}
      }
      else
	gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (w), TRUE);

      set_tip (w, &stock[i]);
      return w;

    default:
      return NULL;
    }
  }
  g_printerr (_("xml_widgets: unrecognised stock control id '%s'\n"), id);
#endif
  return NULL;
}


/*
 * Display widgtets
 */

static GtkWidget *
create_arrow (xml_node_t *node)
/* Properties:
 *	type		string	"right"	arrow direction
 *	shadow		string	"none"	arrow border
 */
{
#ifdef MAKE_TRANSLATION_SOURCE
  return NULL;
#else
  static const char *const arrownames[] = { "up", "down", "left", NULL };
  return gtk_arrow_new (lookup_type (node, "type", arrownames),
			lookup_shadow_type (node));
#endif
}

static GtkWidget *
create_image (xml_node_t *node)
/* Properties:
 *	stock		string	"gtk-stock-missing-image"
 *	size		string	"button"
 */
{
#ifdef MAKE_TRANSLATION_SOURCE
  const char *alt = xml_parser_get_property (node, "alt");
  if (alt && *alt == '~')
  {
    const char *prop = xml_parser_get_property (node, "onclick");
    comment (node, prop ? prop : xml_parser_get_property (node, "stock"));
    translatable (alt);
  }
  return NULL;
#else
  const char *item = xml_parser_get_property (node, "stock");
  if (item && check_stock (item))
    return gtk_image_new_from_stock (item, lookup_image_size (node));
  const char *alt = xml_parser_get_property (node, "alt");
  if (alt)
    return gtk_label_new (TRANSLATE (alt));
  if (item)
    return gtk_image_new_from_icon_name (item, lookup_image_size (node));
  return gtk_image_new_from_stock (GTK_STOCK_MISSING_IMAGE,
				   lookup_image_size (node));
#endif
}

static GtkWidget *
create_label (xml_node_t *node)
/* Properties:
 *	text		string	""
 *	markup		bool	FALSE
 *	mnemonic	string	NULL	reference to another widget (via id)
 *					implies underline usage
 *	width-chars	int	none
 *	wrap		bool	FALSE
 *	... - fixme
 */
{
#ifdef MAKE_TRANSLATION_SOURCE
  const char *text = xml_parser_get_property (node, "text");
  if (text && *text == '~')
    translatable (text);
  return NULL;
#else
  const char *mnemonic = xml_parser_get_property (node, "mnemonic");
  GtkWidget *label = gtk_label_new
		       (TRANSLATE (xml_parser_get_property (node, "text")));
  g_object_set (G_OBJECT (label),
	"use-markup", xml_parser_get_bool (node, "markup"),
	"use-underline", mnemonic && *mnemonic,
	"width-chars", xml_parser_get_property_int (node, "width-chars", -1),
	"wrap", xml_parser_get_bool (node, "wrap"),
	NULL);
  if (mnemonic && *mnemonic)
    mnemonic_list = g_slist_append (mnemonic_list,
				    widget_ref_new (label, mnemonic));
  return label;
#endif
}


/*
 * Container widgets
 */

static GtkWidget *
create_box (xml_node_t *node, gboolean vertical)
/* Properties:
 *	homogeneous	bool	false
 *	spacing		int	0	spacing between items
 *	expand		bool	false	child property
 *	fill		bool	false	child property
 *	padding	int	2	child property
 * Child properties:
 *	box-expand	bool	=expand
 *	box-fill	bool	=fill
 *	box-padding	int	=padding
 *
 * May contain <end /> to mark where packing is switched from start
 */
{
#ifndef MAKE_TRANSLATION_SOURCE
  gboolean homogeneous = xml_parser_get_bool (node, "homogeneous");
  gboolean expand = xml_parser_get_bool (node, "expand");
  gboolean fill = xml_parser_get_bool (node, "fill");
  int padding = xml_parser_get_property_int (node, "spacing", 0);

  GtkWidget *box = vertical
		 ? gtk_vbox_new (homogeneous, padding)
		 : gtk_hbox_new (homogeneous, padding);
  typeof (gtk_box_pack_start) *func = gtk_box_pack_start;

  padding = xml_parser_get_property_int (node, "padding", 2);
#endif

  foreach_glist (node, node->child)
    if (strcasecmp (node->name, "end"))
#ifdef MAKE_TRANSLATION_SOURCE
      widget_parse (node);
  return NULL;
#else
      func (GTK_BOX (box), widget_parse (node),
	    xml_parser_get_property_int (node, "box-expand", expand),
	    xml_parser_get_property_int (node, "box-fill", fill),
	    xml_parser_get_property_int (node, "box-padding", padding));
    else
      func = gtk_box_pack_end;
  return box;
#endif
}

static GtkWidget *
create_table (xml_node_t *node)
/* <table> properties:
 *	rows		int	1		range 1..100
 *	cols		int	1		range 1..100
 *	homogeneous	bool	FALSE
 *	x-padding	int	0		cell property
 *	y-padding	int	=x-padding	cell property
 *	x-attach	string	expand,fill	cell property
 *	y-attach	string	=x-attach	cell property
 * The padding and attachment options can be overridden in <tr> and <td>.
 *
 * <tr>: child of <table>, has no other properties
 *
 * <td>: child of <tr>
 * Properties:
 *	colspan		int	1
 *	rowspan		int	1
 *
 * Cell content which would lie beyond the specified table limits will NOT be
 * created. You do *not* get an automatic box in each cell.
 */
{
#define GET_X_PAD(NODE,v) xml_parser_get_property_int ((NODE), "x-padding", (v))
#define GET_Y_PAD(NODE,d,v) \
  (strcasecmp (xml_parser_get_property ((NODE), "y-padding") ? : "", "x") \
   ? (d) : xml_parser_get_property_int ((NODE), "y-padding", (v)))
#define GET_X_ATTACH(NODE,v) parse_attach ((NODE), "x-attach", (v))
#define GET_Y_ATTACH(NODE,d,v) \
  (strcasecmp (xml_parser_get_property ((NODE), "y-attach") ? : "", "x") \
   ? (d) : parse_attach ((NODE), "y-attach", (v)))

  int rows = xml_parser_get_property_int (node, "rows", 1);
  int cols = xml_parser_get_property_int (node, "cols", 1);
#ifndef MAKE_TRANSLATION_SOURCE
  int xpad = GET_X_PAD (node, 0);
  int ypad = GET_Y_PAD (node, -1, xpad);
  int xatt = GET_X_ATTACH (node, GTK_EXPAND | GTK_FILL);
  int yatt = GET_Y_ATTACH (node, -1, xatt);
#endif
  if (rows < 1 || rows > 100 || cols < 1 || cols > 100)
    g_printerr (_("warning: table rows, cols range is 1 to 100 - clipping\n"));

  rows = MIN (100, MAX (1, rows));
  cols = MIN (100, MAX (1, cols));

  {
#ifndef MAKE_TRANSLATION_SOURCE
    GtkWidget *table =
      gtk_table_new (rows, cols, xml_parser_get_bool (node, "homogeneous"));
#endif
    gboolean occupied[rows][cols];
    int row = 0;
    xml_node_t *rownode;

    memset (occupied, 0, rows * cols * sizeof (gboolean));

    foreach_glist (rownode, node->child)
    {
      if (!strcasecmp (rownode->name, "tr"))
      {
	xml_node_t *cellnode;
#ifndef MAKE_TRANSLATION_SOURCE
	int rxpad = GET_X_PAD (rownode, xpad);
	int rypad = GET_Y_PAD (rownode, rxpad, ypad);
	int rxatt = GET_X_ATTACH (rownode, xatt);
	int ryatt = GET_Y_ATTACH (rownode, rxatt, yatt);
#endif
	int col = 0;
	foreach_glist (cellnode, rownode->child)
	{
	  while (col < cols && occupied[row][col])
	    ++col;
	  if (col == cols)
	    break;

	  if (!strcasecmp (cellnode->name, "td"))
	  {
	    int r, c;
#ifndef MAKE_TRANSLATION_SOURCE
	    int xa, ya;
#endif
	    int width = xml_parser_get_property_int (node, "colspan", 1);
	    int height = xml_parser_get_property_int (node, "rowspan", 1);
	    height = MIN (MAX (1, height), rows - row);
	    width = MIN (MAX (1, width), cols - col);
	    /* mark cells as occupied */
	    for (r = row; r < rows && r < row + height; ++r)
	      for (c = col; c < cols && c < col + width; ++c)
		occupied[r][c] = TRUE;
	    /* create & attach the child element (implied hbox) */
#ifdef MAKE_TRANSLATION_SOURCE
	    widget_parse (cellnode->child);
#else
	    c = GET_X_PAD (cellnode, rxpad);
	    r = GET_Y_PAD (cellnode, c, rypad);
	    xa = GET_X_ATTACH (cellnode, rxatt);
	    ya = GET_Y_ATTACH (cellnode, xa, ryatt);
	    gtk_table_attach (GTK_TABLE (table),
			      widget_parse (cellnode->child),
			      col, col + width, row, row + height,
			      xa, ya == -1 ? xa : ya,
			      c, r == -1 ? c : r);
#endif
	    if (++col == cols)
	      break;
	  }
	}
	if (++row == rows)
	  break;
      }
    }

#ifndef MAKE_TRANSLATION_SOURCE
    row = xml_parser_get_property_int (node, "spacing", 0);
    g_object_set (G_OBJECT (table),
		  "column-spacing",
		  xml_parser_get_property_int (node, "column-spacing", row),
		  "row-spacing",
		  xml_parser_get_property_int (node, "row-spacing", row),
		  NULL);

    return table;
#endif
  }
  return NULL;
}


/*
 * Top-level stuff
 */

static GtkWidget *
widget_parse (xml_node_t *node)
{
  GtkWidget *widget = NULL;
  switch (tolower (node->name[0]))
  {
  case 'a':
    if (!strcasecmp (node->name + 1, "rrow"))
      widget = create_arrow (node);
    break;

  case 'b':
    if (!strcasecmp (node->name + 1, "utton"))
      widget = create_button (node, TRUE);
    break;

  case 'c':
    if (!strcasecmp (node->name + 1, "ontrol"))
      widget = create_stock_gxine (node);
    break;

/*
  case 'f':
    if (!strcasecmp (node->name + 1, "lipbutton"))
      widget = create_flip_button (node);
    break;
*/

  case 'h':
    if (!strcasecmp (node->name + 1, "box"))
      widget = create_box (node, FALSE);
#ifndef MAKE_TRANSLATION_SOURCE
    else if (!strcasecmp (node->name + 1, "separator"))
# if GTK_CHECK_VERSION(3, 2, 0)
      widget = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
# else
      widget = gtk_hseparator_new ();
# endif
#endif
    break;

  case 'i':
    if (!strcasecmp (node->name + 1, "mage"))
      widget = create_image (node);
    break;

  case 'l':
    if (!strcasecmp (node->name + 1, "abel"))
      widget = create_label (node);
    break;

  case 't':
    if (!strcasecmp (node->name + 1, "able"))
      widget = create_table (node);
#ifndef MAKE_TRANSLATION_SOURCE
    else if (!strcasecmp (node->name + 1, "ime"))
    {
      widget = create_time_widget (xml_parser_get_bool (node, "small"),
				   (void *)&timewidgets);
      timewidgets = g_slist_append (timewidgets, widget);
    }
    else if (!strcasecmp (node->name + 1, "itle"))
    {
      widget = create_infobar (xml_parser_get_bool (node, "small"));
      infobars = g_slist_append (infobars, widget);
    }
#endif
    break;

  case 'v':
    if (!strcasecmp (node->name + 1, "box"))
      widget = create_box (node, TRUE);
#ifndef MAKE_TRANSLATION_SOURCE
    else if (!strcasecmp (node->name + 1, "separator"))
# if GTK_CHECK_VERSION(3, 2, 0)
      widget = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
# else
      widget = gtk_vseparator_new ();
# endif
#endif
    break;
  }

#ifdef MAKE_TRANSLATION_SOURCE
  {
    const char *text = xml_parser_get_property (node, "tip");
    if (text && *text == '~')
    {
      comment (node, xml_parser_get_property (node, "onclick"));
      translatable (text);
    }
  }
#else
  if (widget)
  {
    const char *name;

    gtk_widget_set_size_request
      (widget, xml_parser_get_property_int (node, "width", -1),
       xml_parser_get_property_int (node, "height", -1));

    if (xml_parser_get_bool (node, "accept-drop"))
      drag_drop_setup (widget,
		       xml_parser_get_property_int (node, "autoplay", TRUE));

    name = xml_parser_get_property (node, "name");
    if (name && name[0])
      gtk_widget_set_name (widget, name);

    name = xml_parser_get_property (node, "tip");
    if (name)
      gtk_widget_set_tooltip_text (widget, TRANSLATE (name));

    name = xml_parser_get_property (node, "id");
    if (name && *name)
    {
      se_o_t *se_widget;
      char *se_name, *se_dot, *tmp_name;
      static const se_f_def_t defs[] = {
	{ "set_show", js_set_show, 0, JSFUN_FAST_NATIVE, SE_GROUP_HIDDEN, NULL, NULL },
	{ "set_sensitive", js_set_sensitive, 0, JSFUN_FAST_NATIVE, SE_GROUP_HIDDEN, NULL, NULL },
	{ "activate", js_activate, 0, JSFUN_FAST_NATIVE, SE_GROUP_HIDDEN, NULL, NULL },
	{ NULL }
      };
      if (!se_widget_space)
	se_widget_space = se_create_object (gse, NULL, "widget", NULL,
					    SE_GROUP_HIDDEN, NULL);
      se_widget = se_widget_space;
      se_dot = se_name = tmp_name = strdup (name);
      for (; se_name;)
      {
	se_dot = strchr (se_dot, '.');
	if (se_dot)
	  *se_dot++ = 0;
	se_widget = se_find_object (gse, se_widget, se_name)
		    ? : se_create_object (gse, se_widget, se_name, widget,
					  SE_GROUP_HIDDEN, NULL);
	if (!se_widget)
	{
	  g_printerr (_("xml_widgets: failed to create JS object 'widget.%s'\n"),
		   name);
	  break;
	}
	se_name = se_dot;
      }
      if (!se_widget)
      {
	free (tmp_name);
	return widget; /* no point in continuing... */
      }

      se_defuns (gse, se_widget, defs);
      widget_id_list = g_slist_append (widget_id_list,
				       widget_ref_new (widget, name));
      free (tmp_name);
    }

    name = xml_parser_get_property (node, "onshow");
    if (name)
      command_attach (widget, "show", "js-onshow", name, _("XML widget show"));

    name = xml_parser_get_property (node, "onhide");
    if (name)
      command_attach (widget, "hide", "js-onhide", name, _("XML widget hide"));

    name = xml_parser_get_property (node, "listen");
    if (name)
    {
      const char *cmd = xml_parser_get_property (node, "onchange");
      if (cmd)
      {
	g_object_set_data_full (G_OBJECT (widget), "js-listen", g_strdup (cmd),
				destroy_notify);
	se_prop_add_listener (gse, se_get_object (gse, NULL, name), "v",
			      js_listen, G_OBJECT (widget));
      }
    }
  }
#endif

  return widget;
}

static GtkWidget *
widget_create_from_xml_internal (const char *file, const char *name,
				 const char *title, gboolean window,
				 gboolean warn_missing)
{
  GtkWidget *widget = NULL;
  GError *error = NULL;
  xml_node_t *tree, *node;
  char *xml_data;
#ifndef MAKE_TRANSLATION_SOURCE
  GSList *mnemonic;

  mnemonic_list = widget_id_list = NULL;
  xml_domain = NULL;
#endif

  g_file_get_contents (file, &xml_data, NULL, &error);
  if (!xml_data)
  {
    /* we don't always want to report "not found" errors */
    if (warn_missing || error->domain != G_FILE_ERROR ||
	error->code != G_FILE_ERROR_NOENT)
      xml_error ("xml_widgets: %s\n", error->message);
    g_clear_error (&error);
    return NULL;
  }

  xml_parser_init_R (xml_parser_t *xml, xml_data, strlen (xml_data), XML_PARSER_CASE_INSENSITIVE);
  if (xml_parser_build_tree_R (xml, &tree) < 0)
  {
    xml_error (_("xml_widgets: error in %s: XML parser failed\n"), file);
    xml_parser_finalize_R (xml);
    free (xml_data);
    return NULL;
  }

  node = tree;
  if (!strcasecmp (node->name, "gettext"))
  {
#ifndef MAKE_TRANSLATION_SOURCE
    const char *p = xml_parser_get_property (node, "domain");
    if (p && *p)
    {
      char *slash = strrchr (file, '/');
      /* assert (slash && slash > file); */
      *slash = 0;
      xml_domain = g_strconcat (PACKAGE".theme.", p, NULL);
      bindtextdomain (xml_domain, file);
      bind_textdomain_codeset (xml_domain, "UTF-8");
      *slash = '/';
    }
    else
      xml_domain = NULL;
#endif
    node = node->next;
  }

  if (!strcasecmp (node->name, "window"))
  {
#ifdef MAKE_TRANSLATION_SOURCE
    widget_parse (node->child);
#else
    if (!window)
      g_printerr (_("xml_widgets: sorry, <window> is not allowed in %s\n"), file);
    else
    {
      widget = gtk_window_new (GTK_WINDOW_TOPLEVEL);
      if (name)
	gtk_widget_set_name (widget, name);
      gtk_window_set_title (GTK_WINDOW (widget), title);
      gtk_container_add (GTK_CONTAINER (widget), widget_parse (node->child));
      gtk_window_set_resizable (GTK_WINDOW (widget), FALSE);
      g_signal_connect (G_OBJECT (widget), "delete-event",
			G_CALLBACK (gtk_widget_hide_on_delete), NULL);
    }
#endif
  }
#ifndef MAKE_TRANSLATION_SOURCE
  else if (window)
    g_printerr (_("xml_widgets: sorry, <window> is required in %s\n"), file);
#endif
  else
    widget = widget_parse (node);

  xml_parser_free_tree (tree);
  xml_parser_finalize_R (xml);

#ifndef MAKE_TRANSLATION_SOURCE
  foreach_glist (mnemonic, mnemonic_list)
  {
    struct widget_ref *ref = mnemonic->data;
    GSList *widget_id;
    foreach_glist (widget_id, widget_id_list)
    {
      struct widget_ref *w = widget_id->data;
      if (strcasecmp (w->id, ref->id))
	gtk_label_set_mnemonic_widget (GTK_LABEL (ref->object), w->object);
    }
    free (ref);
  }

  /* free globals */
  g_slist_foreach (widget_id_list, (GFunc) free, NULL);
  g_slist_free (mnemonic_list);
  g_slist_free (widget_id_list);
  g_free (xml_domain);

#endif

  /* free locals */
  free (xml_data);

  return widget;
}

#ifndef MAKE_TRANSLATION_SOURCE
GtkWidget *
widget_create_from_xml (const char *leaf, const char *name, const char *title,
			gboolean window, gboolean warn_missing)
{
  GtkWidget *widget = NULL;
  char *file;

  /* there is global data here :-) */
  pthread_mutex_lock (&xml_lock);

  file = get_config_filename (leaf);
  widget = widget_create_from_xml_internal (file, name, title, window, FALSE);
  if (!widget)
  {
    free (file);
    file = g_build_filename (confdir, leaf, NULL);
    widget = widget_create_from_xml_internal (file, name, title, window,
					      warn_missing);
  }
  free (file);

  /* no re-entrancy issues left... */
  pthread_mutex_unlock (&xml_lock);

  return widget;
}
#endif

#ifdef MAKE_TRANSLATION_SOURCE
int
main (int argc, char *argv[])
{
  if (argc != 2)
  {
    g_printerr (_("Source file?\n"));
    return 1;
  }

  widget_create_from_xml_internal (argv[1], NULL, NULL, FALSE, TRUE);
}
#endif
