/*
 * Copyright (C) 2004-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
 *
 * various UI bits:
 * - button registry and update (state display etc.)
 * - slider/spinner adjustment creation and update
 */

#include "globals.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

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

/* for VO_CAP_HUE etc. (where available) */
#include <xine/video_out.h>

#include "ui.h"
#include "utils.h"
#include "engine.h"
#include "player.h"
#include "playlist.h"
#include "preferences.h"
#include "gtkvideo.h"

/*
 * Playback status
 * UI button & adjustment handling
 */

gboolean have_config = FALSE;
int config_version = 0;

gboolean no_recursion = FALSE;

gboolean fs_toolbar_at_top = FALSE;
gboolean fs_toolbar_visible = FALSE;
gboolean wm_toolbar_visible = TRUE;
gboolean wm_toolbar_snap = TRUE;

gboolean fs_always_sticky = FALSE;
gboolean fs_is_sticky = FALSE;

GtkIconSize icon_size_logo;

static ui_status_t status = UI_CURRENT_STATE;
pthread_mutex_t status_lock = PTHREAD_MUTEX_INITIALIZER;

static GSList *c_buttons[Control_Buttons] = { NULL };
static GtkAdjustment *c_adjustments[Control_Adjustments] = { NULL };
static GSList *c_adj_widgets[Control_Adjustments] = { NULL };

/* (Internal) Reflect play/pause/stop status in control buttons */

static void ui_set_list (GSList **list, int index, const char *property, gboolean state)
{
  GSList *btn;
  foreach_glist (btn, list[index])
    if (g_object_class_find_property (G_OBJECT_GET_CLASS (btn->data), property))
      g_object_set (G_OBJECT (btn->data), property, state, NULL);
}

static void ui_int_set_status (ui_status_t status)
{
  char states[Control_PlayerButtons] = { 0 };
  int live = player_live_stream () || playlist_showing_logo ();
  int i;

  switch (status)
  {
  case UI_STOP:		states[Control_STOP]	= 1; break;
  case UI_PAUSE:	states[Control_PAUSE]	= 1; break;
  case UI_PLAY_SLOW:	states[Control_PLAY]	= 1;
			states[Control_PAUSE]	= 1; break;
  case UI_PLAY:		states[Control_PLAY]	= 1; break;
  case UI_FAST_FORWARD:	states[Control_FASTFWD]	= 1; break;
  default:;
  }

  no_recursion = TRUE;
  for (i = 0; i < Control_PlayerButtons; ++i)
  {
    ui_set_list (c_buttons, i, "active", states[i]);
    if (i == Control_PLAY)
      ui_set_list (c_buttons, i, "sensitive", !!playlist_size ());
    else if (i == Control_PAUSE || i == Control_REWIND || i == Control_FASTFWD)
      ui_set_list (c_buttons, i, "sensitive", !live);
  }
  no_recursion = FALSE;
}

/* (Internal) Reflect mute status in control button */

static void ui_int_set_mute (int mute)
{
  GSList *btn;
  no_recursion = TRUE;
  foreach_glist (btn, c_buttons[Control_MUTE])
    g_object_set (G_OBJECT(btn->data), "active", !mute, NULL);
  no_recursion = FALSE;
}

/* Set/show play/pause/stop/mute status in control buttons */

void ui_set_status (ui_status_t newstatus)
{
  pthread_mutex_lock (&status_lock);


  switch (newstatus)
  {
  case UI_CURRENT_STATE:
    ui_int_set_status (status);
    break;
  case UI_STOP:
  case UI_PAUSE:
  case UI_PLAY_SLOW:
  case UI_PLAY:
  case UI_FAST_FORWARD:
    if (playlist_showing_logo ())
      newstatus = UI_STOP;
    ui_int_set_status (status = newstatus);
    break;
  case UI_AUDIO_UNMUTE:	ui_int_set_mute (FALSE); break;
  case UI_AUDIO_MUTE:	ui_int_set_mute (TRUE); break;
  case UI_FS_TOOLBAR:	/* nothing to do */; break;
  case UI_FS_TOOLBAR_POS:/* nothing to do */; break;
  case UI_WM_TOOLBAR:	/* nothing to do */; break;
  case UI_WM_TOOLBAR_SNAP:/* nothing to do */; break;
  default:;
  }

  pthread_mutex_unlock (&status_lock);
}

/* Register a button for the above status functions */

void ui_register_control_button (control_button_t item, GtkWidget *widget)
{
  g_return_if_fail (item < Control_Buttons);
  c_buttons[item] = g_slist_append (c_buttons[item], widget);
}

/* Adjustment object <=> adjustment type <=> JS object */

typedef struct {
  gdouble start, min, max, step, page, pagesize;
  const char *setting, *jspref;
  int param;
#ifdef VO_CAP_HUE
  enum { CAP_NONE, CAP_AUDIO, CAP_VIDEO } type;
  int cap;
#endif
} ui_adjustable_t;

#ifdef VO_CAP_HUE
# define XINE_PARAM_VO(X) XINE_PARAM_VO_##X, CAP_VIDEO, VO_CAP_##X
#else
# define XINE_PARAM_VO(X) XINE_PARAM_VO_##X
#endif

static const ui_adjustable_t ranges[] = {
  [Control_SEEKER]		= {     0,       0,  65535,    1,    10, 0, NULL },
  [Control_AUDIO_CHANNEL]	= {    -1,      -1,     32,    1,     1, 0, NULL,			 "ao_channel",	  XINE_PARAM_AUDIO_CHANNEL_LOGICAL },
  [Control_SPU_CHANNEL]	        = {    -1,      -1,     32,    1,     1, 0, NULL,			 "spu_channel",	  XINE_PARAM_SPU_CHANNEL },
  [Control_VOLUME]		= {    50,       0,    100,    1,    10, 0, "audio.volume.mixer_volume", "ao_volume",	  XINE_PARAM_AUDIO_VOLUME },
  [Control_COMPRESSOR]		= {   100,     100,   1000,   10,   100, 0, "gui.ao_compressor",	 "ao_compressor", XINE_PARAM_AUDIO_COMPR_LEVEL },
  [Control_AMPLIFIER]		= {   100,       0,    200,    1,    10, 0, "gui.ao_amplifier",		 "ao_amplifier",  XINE_PARAM_AUDIO_AMP_LEVEL },
  [Control_AV_SYNC]		= {     0, -180000, 180000, 1000, 10000, 0, "gui.av_sync",		 "av_sync",	  XINE_PARAM_AV_OFFSET },
  [Control_SPU_SYNC]		= {     0, -180000, 180000, 1000, 10000, 0, "gui.spu_sync",		 "spu_sync",	  XINE_PARAM_SPU_OFFSET },
  [Control_HUE]			= { 32768,       0,  65535,  100,  1000, 0, "gui.vo_hue",		 "vo_hue",	  XINE_PARAM_VO(HUE) },
  [Control_SATURATION]		= { 32768,       0,  65535,  100,  1000, 0, "gui.vo_saturation",	 "vo_saturation", XINE_PARAM_VO(SATURATION) },
  [Control_CONTRAST]		= { 32768,       0,  65535,  100,  1000, 0, "gui.vo_contrast",		 "vo_contrast",   XINE_PARAM_VO(CONTRAST) },
  [Control_BRIGHTNESS]		= { 32768,       0,  65535,  100,  1000, 0, "gui.vo_brightness",	 "vo_brightness", XINE_PARAM_VO(BRIGHTNESS) },
#ifdef XINE_PARAM_VO_GAMMA
  /* 5957 == (1000 - 100) * 65536 / (10000 - 100); range (on rv370) is 100..10000 */
  [Control_GAMMA]		= {  5957,       0,  65535,  100,  1000, 0, "gui.vo_gamma",		 "vo_gamma",	  XINE_PARAM_VO(GAMMA) },
#endif
#ifdef XINE_PARAM_VO_SHARPNESS
  [Control_SHARPNESS]		= { 32768,       0,  65535,  100,  1000, 0, "gui.vo_sharpness",		 "vo_sharpness",  XINE_PARAM_VO(SHARPNESS) },
  [Control_NOISE_REDUCE]	= {     0,       0,  65535,  100,  1000, 0, "gui.vo_noise_reduction",	 "vo_noise_reduction", XINE_PARAM_VO(NOISE_REDUCTION) },
#endif
  [Control_EQ_30]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_30",		 "eq_30",	  XINE_PARAM_EQ_30HZ },
  [Control_EQ_60]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_60",		 "eq_60",	  XINE_PARAM_EQ_60HZ },
  [Control_EQ_125]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_125",		 "eq_125",	  XINE_PARAM_EQ_125HZ },
  [Control_EQ_250]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_250",		 "eq_250",	  XINE_PARAM_EQ_250HZ },
  [Control_EQ_500]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_500",		 "eq_500",	  XINE_PARAM_EQ_500HZ },
  [Control_EQ_1K]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_1K",		 "eq_1K",	  XINE_PARAM_EQ_1000HZ },
  [Control_EQ_2K]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_2K",		 "eq_2K",	  XINE_PARAM_EQ_2000HZ },
  [Control_EQ_4K]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_4K",		 "eq_4K",	  XINE_PARAM_EQ_4000HZ },
  [Control_EQ_8K]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_8K",		 "eq_8K",	  XINE_PARAM_EQ_8000HZ },
  [Control_EQ_16K]		= {   100,	 0,    100,    1,    10, 0, "gui.eq_16K",		 "eq_16K",	  XINE_PARAM_EQ_16000HZ },
};
static gdouble starts[G_N_ELEMENTS (ranges)];
static gdouble inits[G_N_ELEMENTS (ranges)];
static se_o_t *jsobjs[G_N_ELEMENTS (ranges)];

/* true if not volume setting or audio.remember_volume is set */
static int check_remember_volume (const ui_adjustable_t *info)
{
  xine_cfg_entry_t entry;
  return info->param != XINE_PARAM_AUDIO_VOLUME ||
	 !xine_config_lookup_entry (xine, "audio.volume.remember_volume", &entry) ||
	 entry.num_value;
}

static void ui_adjustment_value_changed_cb (GtkAdjustment *adj,
					    const ui_adjustable_t *info)
{
  xine_cfg_entry_t entry;

  if (no_recursion)
    return;

  if (info->param)
    xine_set_param (stream, info->param, gtk_adjustment_get_value(adj));

  if (info->param == XINE_PARAM_AUDIO_VOLUME)
    return;
  if (info->setting &&
      xine_config_lookup_entry (xine, info->setting, &entry))
  {
    entry.num_value = gtk_adjustment_get_value(adj);
    preferences_update_entry (&entry);
  }
}

static void
ui_seeker_value_changed_cb (GtkAdjustment *seeker, gpointer data)
{
  if (!no_recursion &&
      xine_get_stream_info (stream, XINE_STREAM_INFO_SEEKABLE))
  {
    int speed;
    ++no_recursion;
    speed = xine_get_param (stream, XINE_PARAM_SPEED);
    xine_play (stream, (gint) gtk_adjustment_get_value(seeker), 0);
    xine_set_param (stream, XINE_PARAM_SPEED, speed);
    --no_recursion;
  }
}

GtkAdjustment *ui_register_control_adjustment (control_adjustment_t item)
{
  g_return_val_if_fail (item < Control_Adjustments, NULL);

  if (!c_adjustments[item])
  {
    xine_cfg_entry_t entry;
    int start = ranges[item].start;

    if (ranges[item].setting)
    {
      if (!check_remember_volume (&ranges[item]))
	start = xine_get_param (stream, XINE_PARAM_AUDIO_VOLUME);
      else if (xine_config_lookup_entry (xine, ranges[item].setting, &entry))
	start = entry.num_value;
    }

    starts[item] = start;

    c_adjustments[item] =
      GTK_ADJUSTMENT (gtk_adjustment_new (start, ranges[item].min, ranges[item].max,
                                          ranges[item].step, ranges[item].page,
                                          ranges[item].pagesize));
    if (ranges[item].param || ranges[item].setting)
      g_signal_connect (c_adjustments[item], "value-changed",
			G_CALLBACK(ui_adjustment_value_changed_cb),
			(gpointer) &ranges[item]);
    else if (item == Control_SEEKER) /* special case */
      g_signal_connect (c_adjustments[item], "value-changed",
			G_CALLBACK (ui_seeker_value_changed_cb), NULL);
  }
  return c_adjustments[item];
}

/* Register a widget for the above status functions */

void ui_register_adjustment_widget (control_adjustment_t item, GtkWidget *widget)
{
  g_return_if_fail (item < Control_Adjustments);
  c_adj_widgets[item] = g_slist_append (c_adj_widgets[item], widget);
}

/* Set the state of every button in a class, e.g. every play button */

void ui_set_control_button (control_button_t item, gboolean state)
{
  GSList *btn;
  g_return_if_fail (item >= Control_PlayerButtons && item < Control_Buttons);
  no_recursion = TRUE;
  if (c_buttons[item] &&
      (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (c_buttons[item]->data)) != state))
    foreach_glist (btn, c_buttons[item])
      g_object_set (G_OBJECT(btn->data), "active", state, NULL);
  no_recursion = FALSE;
}

/* Set the value of an adjustment */

void ui_set_control_adjustment (control_adjustment_t item, gdouble value)
{
  g_return_if_fail (item < Control_Adjustments);
  no_recursion = TRUE;
  if (gtk_adjustment_get_value (GTK_ADJUSTMENT (c_adjustments[item])) != value)
    g_object_set (G_OBJECT(c_adjustments[item]), "value", value, NULL);
  no_recursion = FALSE;
}

void ui_reset_control_adjustment (control_adjustment_t item)
{
  g_return_if_fail (item < Control_Adjustments);
  g_object_set (G_OBJECT(c_adjustments[item]), "value",
		ranges[item].start, NULL);
}

void ui_revert_control_adjustment (control_adjustment_t item)
{
  g_return_if_fail (item < Control_Adjustments);
  g_object_set (G_OBJECT(c_adjustments[item]), "value", starts[item], NULL);
}

void ui_clear_control_adjustment (control_adjustment_t item)
{
  g_return_if_fail (item < Control_Adjustments);
  if (check_remember_volume (&ranges[item]))
    xine_set_param (stream, ranges[item].param, inits[item]);
}

/* Update all sliders & spinners which use this adjustment */

void ui_xine_set_param_from_adjustment (control_adjustment_t item)
{
  g_return_if_fail (item < Control_Adjustments);
  gtk_adjustment_value_changed (GTK_ADJUSTMENT(c_adjustments[item]));
}

/* Button creation */

static GtkWidget *ui_container_new_stock (GtkWidget *box, const char *stock)
{
  GtkWidget *img = gtk_image_new_from_stock (stock, GTK_ICON_SIZE_BUTTON);
#if GTK_CHECK_VERSION(3, 14, 0)
  gtk_widget_set_margin_start (img, 0);
  gtk_widget_set_margin_end (img, 0);
  gtk_widget_set_margin_top (img, 0);
  gtk_widget_set_margin_bottom (img, 0);
#else
  gtk_misc_set_padding (GTK_MISC(img), 0, 0);
#endif
  if (box)
    gtk_container_add (GTK_CONTAINER(box), img);
  return box ? box : img;
}

GtkWidget *ui_button_new_stock (const char *stock)
{
  return ui_container_new_stock (gtk_button_new (), stock);
}

GtkWidget *ui_toggle_button_new_stock (const char *stock)
{
  return ui_container_new_stock (gtk_toggle_button_new (), stock);
}

GtkWidget *ui_button_new_stock_mnemonic (const char *stock,
					 const char *mnemonic)
{
  GtkWidget *button = gtk_button_new ();
  GtkWidget *align = gtk_alignment_new (0.5, 0.5, 0, 0);
  GtkWidget *hbox = gtk_hbox_new (FALSE, 2);
  /* hbox in alignment in button => hbox is not expanded to fill the button */
  gtk_container_add (GTK_CONTAINER(align), hbox);
  gtk_container_add (GTK_CONTAINER(button), align);
  gtk_box_pack_start (GTK_BOX(hbox), ui_container_new_stock (NULL, stock),
		      FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX(hbox), gtk_label_new_with_mnemonic (mnemonic),
		      FALSE, FALSE, 0);
  return button;
}

static GtkWidget *ui_container_new_icon_name (GtkWidget *box, const char *name)
{
  GtkWidget *img = gtk_image_new_from_icon_name (name, GTK_ICON_SIZE_BUTTON);
#if GTK_CHECK_VERSION(3, 14, 0)
  gtk_widget_set_margin_start (img, 0);
  gtk_widget_set_margin_end (img, 0);
  gtk_widget_set_margin_top (img, 0);
  gtk_widget_set_margin_bottom (img, 0);
#else
  gtk_misc_set_padding (GTK_MISC(img), 0, 0);
#endif
  if (box)
    gtk_container_add (GTK_CONTAINER(box), img);
  return box ? box : img;
}

GtkWidget *ui_button_new_icon_name (const char *name)
{
  return ui_container_new_icon_name (gtk_button_new (), name);
}

GtkWidget *ui_toggle_button_new_icon_name (const char *name)
{
  return ui_container_new_icon_name (gtk_toggle_button_new (), name);
}

GtkWidget *ui_button_new_icon_name_mnemonic (const char *name,
					     const char *mnemonic)
{
  GtkWidget *button = gtk_button_new ();
  GtkWidget *align = gtk_alignment_new (0.5, 0.5, 0, 0);
  GtkWidget *hbox = gtk_hbox_new (FALSE, 2);
  /* hbox in alignment in button => hbox is not expanded to fill the button */
  gtk_container_add (GTK_CONTAINER(align), hbox);
  gtk_container_add (GTK_CONTAINER(button), align);
  gtk_box_pack_start (GTK_BOX(hbox), ui_container_new_icon_name (NULL, name),
		      FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX(hbox), gtk_label_new_with_mnemonic (mnemonic),
		      FALSE, FALSE, 0);
  return button;
}

/* Misc widgets with common properties */

GtkWidget *ui_spin_button_new (GtkAdjustment *adj)
{
  return g_object_new (GTK_TYPE_SPIN_BUTTON, "adjustment", adj, "digits", 0,
		       "update-policy", GTK_UPDATE_ALWAYS, "climb-rate", 1.0,
		       "numeric", TRUE, NULL);
}

GtkWidget *ui_hscale_new (GtkAdjustment *adj, GtkPositionType pos, int digits)
{
  return g_object_new (GTK_TYPE_HSCALE, "adjustment", adj, "value-pos", pos,
		       "digits", digits, NULL);
}

GtkWidget *ui_label_new_with_xalign (const char *label, gfloat x)
{
  return g_object_new (GTK_TYPE_LABEL, "label", label, "xalign", x, NULL);
}

GtkWidget *ui_label_new_with_markup (const char *label)
{
  return g_object_new (GTK_TYPE_LABEL, "label", label, "use-markup", TRUE, NULL);
}

/* Menu/toolbar basics */

GtkUIManager *ui_create_manager (const char *label, GtkWidget *window)
{
  GtkUIManager *ui = gtk_ui_manager_new ();
  GtkActionGroup *action = gtk_action_group_new (label);
#ifdef ENABLE_NLS
  gtk_action_group_set_translation_domain (action, PACKAGE);
#endif
  gtk_ui_manager_insert_action_group (ui, action, 0);
  if (window)
    gtk_window_add_accel_group (GTK_WINDOW(window),
				gtk_ui_manager_get_accel_group (ui));
  return ui;
}

void ui_mark_active (GtkUIManager *ui, const char *const items[],
		     gboolean active)
{
  GtkActionGroup *actions = ui_get_action_group (ui);
  int i;
  for (i = 0; items[i]; ++i)
  {
    gtk_action_set_sensitive (gtk_action_group_get_action (actions, items[i]),
			      active);
  }
}

void gtk_action_group_connect_accelerators (GtkActionGroup *actions)
{
  GList *list = gtk_action_group_list_actions (actions);
  g_list_foreach (list, (GFunc) gtk_action_connect_accelerator, NULL);
  g_list_free (list);
}

/* Toolbar control and JS functions */

static void set_fs_toolbar_top (int v)
{
  if (v)
    gtk_action_activate (action_items.fs_toolbar_pos->next->data); /* top */
  else
    gtk_action_activate (action_items.fs_toolbar_pos->data); /* bottom */
}

#ifdef WITH_DEPRECATED

static JSBool js_fs_toolbar_show (JSContext *cx, uintN argc, jsval *vp)
{
  JSBool show;

  se_log_fncall_deprecated ("toolbar_show");
  se_argc_check_max (1, "toolbar_show");

  jsval *argv = JS_ARGV (cx, vp);
  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "toolbar_show");
    JS_ValueToBoolean (cx, argv[0], &show);
  }
  else
    show = !fs_toolbar_visible;

  window_fs_toolbar_show (show);

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

#ifdef WITH_OBSOLETE

static JSBool js_fs_toolbar_position (JSContext *cx, uintN argc, jsval *vp)
{
  JSBool top;

  se_log_fncall_obsolete ("set_toolbar_position");
  se_argc_check_max (1, "set_toolbar_position");

  jsval *argv = JS_ARGV (cx, vp);
  if (argc == 1)
  {
    se_arg_is_int_or_bool (0, "set_toolbar_position");
    JS_ValueToBoolean (cx, argv[0], &top);
  }
  else
    top = !fs_toolbar_at_top;

  set_fs_toolbar_top
    (gtk_radio_action_get_current_value (action_items.fs_toolbar_pos->data));

  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

#endif /* WITH_OBSOLETE */
#endif /* WITH_DEPRECATED */

/* A/V settings JS methods */

static int ui_lookup_js_obj (const JSObject *obj)
{
  int i;
  for (i = 0; i < (int) G_N_ELEMENTS (ranges); ++i)
    if (jsobjs[i] && jsobjs[i]->obj == obj)
      return i;
  abort (); /* can't happen */
}

static JSBool js_control_revert (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall ("<control>.revert");
  se_argc_check (0, "<control>.revert");
  ui_revert_control_adjustment (ui_lookup_js_obj (JS_THIS_OBJECT (cx, vp)));
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_control_reset (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall ("<control>.reset");
  se_argc_check (0, "<control>.reset");
  ui_clear_control_adjustment (ui_lookup_js_obj (JS_THIS_OBJECT (cx, vp)));
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

/* Integer or boolean properties */

static void ui_prop_get (const se_prop_t *prop, se_prop_read_t *value)
{
  const ui_property_t *ui = (void *)prop->data;
  switch (ui->type)
  {
  case UI_PROP_BOOL:
    value->i = *ui->ui.flag.flag;
    break;
  case UI_PROP_XINE_PARAM:
    value->i = xine_get_param (stream, ui->ui.xine.param);
    break;
  case UI_PROP_GTK_WIDGET:
    value->i = ui->ui.widget.get (*ui->ui.widget.widget);
    break;
  case UI_PROP_GTK_VIDEO:
    value->i = ui->ui.video.get (GTK_VIDEO (gtv));
    break;
  case UI_PROP_CUSTOM:
    ui->ui.custom.get (ui, value);
    break;
  }
}

int ui_property_clip_int (const ui_property_t *prop, int value)
{
  if (prop->loop)
  {
    while (value < prop->min)
      value += prop->max - prop->min + 1;
    while (value > prop->max)
      value -= prop->max - prop->min + 1;
  }
  else if (value < prop->min)
    value = prop->min;
  else if (value > prop->max)
    value = prop->max;

  return value;
}

static int ui_prop_set_internal (ui_property_t *prop, int data)
{
  switch (prop->type)
  {
  case UI_PROP_BOOL:
    if (prop->ui.flag.set)
      prop->ui.flag.set (data);
    else if (gtk_toggle_action_get_active (*prop->ui.flag.action) != data)
      gtk_action_activate (&(*prop->ui.flag.action)->parent);
    break;
  case UI_PROP_XINE_PARAM:
    if (prop->ui.xine.set)
      prop->ui.xine.set (data);
    else
      xine_set_param (stream, prop->ui.xine.param, data);
    break;
  case UI_PROP_GTK_WIDGET:
    prop->ui.widget.set (*prop->ui.widget.widget, data);
    break;
  case UI_PROP_GTK_VIDEO:
    if (gtk_toggle_action_get_active (*prop->ui.video.action) != data)
      gtk_action_activate (&(*prop->ui.video.action)->parent);
    break;
  case UI_PROP_CUSTOM:
    prop->ui.custom.set (data);
    break;
  }
  return 0;
}

static int ui_prop_set_int (void *data, se_t *se, se_o_t *o,
			    se_prop_t *se_prop, se_prop_read_t value)
{
  value.i = ui_property_clip_int (data, value.i);
  return ui_prop_set_internal (data, value.i);
}

static int ui_prop_set_bool (void *data, se_t *se, se_o_t *o,
			     se_prop_t *se_prop, se_prop_read_t value)
{
  return ui_prop_set_internal (data, !!value.i);
}

static JSBool ui_prop_toggle_bool (JSContext *cx, uintN argc, jsval *vp)
{
  se_t *se = (se_t *) JS_GetContextPrivate(cx);
  se_o_t *o = JS_GetPrivate (cx, JS_THIS_OBJECT (cx, vp));
  JSBool v = !se_prop_get_bool (se, o, "v");
  JS_SET_RVAL (cx, vp, BOOLEAN_TO_JSVAL (v));
  se_prop_set_bool (se, o, "v", v);
  return JS_TRUE;
}

void ui_create_properties (const ui_property_t *prop, se_o_t *parent,
			   se_prop_type_t type)
{
  int i;
  se_prop_cb_t cb = NULL;

  switch (type)
  {
  case SE_TYPE_INT: cb = ui_prop_set_int; break;
  case SE_TYPE_BOOL: cb = ui_prop_set_bool; break;
  default:;
  }

  for (i = 0; prop[i].name; ++i)
  {
    se_o_t *obj =
      se_create_object (gse, parent, prop[i].name, NULL, SE_GROUP_PROPERTIES,
			gettext (prop[i].help));
    se_prop_create_with_reader (gse, obj, "v", "", type, (intptr_t) &prop[i],
				FALSE, ui_prop_get);

    if (prop[i].listen.cb)
      se_prop_add_listener (gse, obj, "v", prop[i].listen.cb,
			    prop[i].listen.user_data);
    else
      se_prop_add_listener (gse, obj, "v", cb, (void *)&prop[i]);

    switch (type)
    {
    case SE_TYPE_INT:
      se_prop_create_int (gse, obj, "min", prop[i].min, TRUE);
      se_prop_create_int (gse, obj, "max", prop[i].max, TRUE);
      break;
    case SE_TYPE_BOOL:
      se_defun (gse, obj, "toggle", ui_prop_toggle_bool, 0, JSFUN_FAST_NATIVE,
		SE_GROUP_HIDDEN, NULL, NULL);
      break;
    default:; /* we don't handle string and float */
    }
  }
}

/* Post-plugin config item callbacks */

static void post_deinterlace_plugin_cb (void *data, xine_cfg_entry_t *cfg)
{
  if (gtv)
    gtk_video_set_post_plugins_deinterlace ((GtkVideo *)gtv, cfg->str_value);
}

static void post_plugins_video_cb (void *data, xine_cfg_entry_t *cfg)
{
  if (gtv)
    gtk_video_set_post_plugins_video ((GtkVideo *)gtv, cfg->str_value);
}

static void post_plugins_audio_cb (void *data, xine_cfg_entry_t *cfg)
{
  if (gtv)
    gtk_video_set_post_plugins_audio ((GtkVideo *)gtv, cfg->str_value,
				      audio_port);
}

/* Initialisation */

void ui_preferences_register (xine_t *this)
{
  static const char * const experience_labels[] = {
    N_("Beginner"), N_("Advanced"), N_("Expert"),
    N_("Master of the known universe"), NULL
  };
  static const char * const tbar_pos_labels[] = {
    N_("Top, hidden"), N_("Bottom, hidden"),
    N_("Top, visible"), N_("Bottom, visible"),
    NULL
  };
  const char **vis_labels;
  const char * const *vis_src;
  unsigned int i;

  config_version = xine_config_register_num
    (this, "misc.gxine_config", 0, "gxine internal", NULL, 32767, NULL, NULL);
  xine_cfg_entry_t entry;
  if (xine_config_lookup_entry (xine, "misc.gxine_config", &entry))
  {
    entry.num_value = CONFIG_VERSION_CURRENT;
    xine_config_update_entry (xine, &entry);
  }

  /* Register the experience level setting */
  xine_config_register_enum
    (this, "gui.experience_level", 0, (char **)experience_labels,
     _("Display of configuration settings"),
     _("Controls whether more advanced configuration settings are shown."),
     0, GXINE_TRUE, NULL);

  /* Register a few audio/video config items */
  for (i = 0; i < G_N_ELEMENTS (ranges); ++i)
  {
    if (ranges[i].setting && !strncmp (ranges[i].setting, "gui.", 4))
      xine_config_register_range (this, ranges[i].setting, ranges[i].start,
				  ranges[i].min, ranges[i].max, NULL, NULL,
				  40, NULL, NULL);
    starts[i] = inits[i] = ranges[i].start;
  }

  /* Register some front-end widget options */
  xine_config_register_string
    (this, "gui.post_plugins.deinterlace",
     "tvtime:method=LinearBlend,cheap_mode=1,pulldown=none,use_progressive_frame_flag=1",
     _("Deinterlace plugins' names and parameters"),
     _("Format: plugin:arg=value,arg=value,...;plugin:..."),
     40, post_deinterlace_plugin_cb, CONFIG_DATA_NONE);

  xine_config_register_bool
    (this, "gui.post_plugins.deinterlace_enable", 1,
     _("Enable deinterlacing at startup"), NULL,
     30, NULL, NULL);

  xine_config_register_string
    (this, "gui.post_plugins.video", "",
     _("Video post-processing plugins' names and parameters"),
     _("Format: plugin:arg=value,arg=value,...;plugin:..."),
     40, post_plugins_video_cb, CONFIG_DATA_NONE);

  xine_config_register_bool
    (this, "gui.post_plugins.video_enable", CONFIG_VERSION_ATLEAST (0, 6, 0),
     _("Enable video post-processing at startup"), NULL,
     30, NULL, NULL);
  config_update_default ("gui.post_plugins.video_enable", 1);

  xine_config_register_string
    (this, "gui.post_plugins.audio", "",
     _("Audio post-processing plugins' names and parameters"),
     _("Format: plugin:arg=value,arg=value,...;plugin:..."),
     40, post_plugins_audio_cb, CONFIG_DATA_NONE);

  xine_config_register_bool
    (this, "gui.post_plugins.audio_enable", CONFIG_VERSION_ATLEAST (0, 6, 0),
     _("Enable audio post-processing at startup"), NULL,
     30, NULL, NULL);
  config_update_default ("gui.post_plugins.audio_enable", 1);

  vis_src = xine_list_post_plugins_typed
	      (xine, XINE_POST_TYPE_AUDIO_VISUALIZATION);
  int goom = 0;
  for (i = 0; vis_src[i]; ++i)
    if (!strcmp (vis_src[i], "goom"))
      goom = i + 1;
  vis_labels = calloc (i + 2, sizeof (char *));
  vis_labels[0] = N_("None"); /* translate for display, not for config file */
  memcpy (vis_labels + 1, vis_src, (i + 1) * sizeof (char *));

  xine_config_register_enum
    (this, "gui.post_plugins.audio_visualisation", goom, (char **)vis_labels,
     _("Default audio visualisation plugin"),
     _("Post-plugin to be used when playing streams without video"),
     10, NULL, NULL);

  xine_config_register_bool
    (this, "gui.post_plugins.always_show_logo", 1,
     _("In windowed mode, whether the logo is shown after playing an audio-only stream if no audio visualisation was active."), NULL,
     0, GXINE_TRUE, NULL);

  fs_is_sticky = fs_always_sticky = xine_config_register_bool
    (this, "gui.fullscreen_mode.always_sticky", 1,
     _("In full-screen mode, whether the video always appears on all desktops & viewports"),
     NULL, 0, NULL, NULL);

  xine_config_register_enum
    (this, "gui.fullscreen_mode.toolbar", 1, (char **)tbar_pos_labels,
     _("Default position & visibility of the full-screen toolbar"),
     NULL, 0, NULL, NULL);

  xine_config_register_bool
    (this, "gui.windowed_mode.separate_toolbar", CONFIG_VERSION_ATLEAST (0, 6, 0),
     _("In windowed mode, whether the toolbar is in a separate window"), NULL,
     0, NULL, NULL);
  config_update_default ("gui.windowed_mode.separate_toolbar", 1);

  xine_config_register_bool
    (this, "gui.windowed_mode.unblank", CONFIG_VERSION_ATLEAST (0, 6, 0),
     _("In windowed mode, prevent blanking when playing video"), NULL,
     0, NULL, NULL);
  config_update_default ("gui.windowed_mode.unblank", 1);

  xine_config_register_bool
    (this, "gui.show_splash", 1,
     _("Display splash screen"),
     _("If enabled, gxine will display its splash screen"),
     0, NULL, NULL);
}

static int ui_listener (void *data, se_t *se, se_o_t *o, se_prop_t *prop,
			se_prop_read_t value)
{
  int i = (ui_adjustable_t *)data - ranges;
  if (value.i < ranges[i].min)
    value.i = ranges[i].min;
  else if (value.i > ranges[i].max)
    value.i = ranges[i].max;
  ui_set_control_adjustment (i, value.i);
  ui_xine_set_param_from_adjustment (i);
  return 0;
}

static GtkIconSource *make_icon_source (const char *name, GtkStateType state,
					GtkIconSize size)
{
  GtkIconSource *isrc = gtk_icon_source_new ();
  char *filename = *name == '/'
		   ? g_build_filename (icondir, name, NULL)
		   : g_build_filename (pixmapdir, name, NULL);
  gtk_icon_source_set_filename (isrc, filename);
  free (filename);
  gtk_icon_source_set_direction_wildcarded (isrc, TRUE);
  gtk_icon_source_set_size_wildcarded (isrc, (int) size == -1);
  if ((int) size != -1)
    gtk_icon_source_set_size (isrc, size);
  gtk_icon_source_set_state_wildcarded (isrc, (int) state == -1);
  if ((int) state != -1)
    gtk_icon_source_set_state (isrc, state);
  return isrc;
}

static GtkIconSet *ui_std_icon (GtkIconFactory *ifactory, const char *name,
				const char *file)
{
  GtkStockItem stock;
  if (!gtk_stock_lookup (name, &stock))
  {
    GtkIconSet *iset = gtk_icon_set_new ();
    gtk_icon_set_add_source (iset, make_icon_source (file, -1, -1));
    gtk_icon_factory_add (ifactory, name, iset);
    return iset;
  }
  return NULL;
}

static void ui_load_icons (void)
{
  GtkIconFactory *ifactory = gtk_icon_factory_new ();

  ui_std_icon (ifactory, GXINE_MEDIA_SPEAKER, "speaker.png");
  ui_std_icon (ifactory, GXINE_MEDIA_SPEAKER_MUTE, "nospeaker.png");
  ui_std_icon (ifactory, GXINE_MEDIA_MARK, "/gxine.png");
  ui_std_icon (ifactory, GXINE_LOGO, "/gxine.png");

  static const char *const icons[][2] = {
    { "gxine-settings-av-sync", "set-sync" },
    { "gxine-settings-spu", "subtitle" },
    { "gxine-settings-hue", "set-hue" },
    { "gxine-settings-saturation", "set-saturation" },
    { "gxine-settings-contrast", "set-contrast" },
    { "gxine-settings-brightness", "set-brightness" },
//  { "gxine-settings-gamma", "set-gamma" },
    { "gxine-settings-sharpness", "set-sharpness" },
//  { "gxine-settings-noise-reduce", "set-noise-reduction" },
    { "gxine-settings-volume", "set-volume" },
//  { "gxine-settings-compressor", "set-compressor" },
//  { "gxine-settings-amplifier", "set-amplifier" },
    { "gxine-settings-audio-channel", "set-channel" },
  };

  /* Do we have an SVG loader? If so, use SVG, else use PNG */
  GSList *fmt, *fmts = gdk_pixbuf_get_formats ();
  char *name = NULL;
  foreach_glist (fmt, fmts)
  {
    free (name);
    name = gdk_pixbuf_format_get_name (fmt->data);
    if (!strcmp (name, "svg"))
      break;
  }
  free (name);
  g_slist_free (fmts);
  int i;
  for (i = 0; i < G_N_ELEMENTS (icons); ++i)
  {
    char name[128];
    snprintf (name, sizeof(name), "%s.%s", icons[i][1], fmt ? "svg" : "png");
    ui_std_icon (ifactory, icons[i][0], name);
  }

#ifdef WITH_OBSOLETE
  ui_std_icon (ifactory, "gxine-logo", "/gxine.png");
#endif

  icon_size_logo = gtk_icon_size_register ("gxine-logo-size", 64, 48);

  gtk_icon_factory_add_default (ifactory);
}

/* GtkEntry clipboard paste and button press overrides */

static void (*old_gtk_entry_paste) (GtkEntry *);
#if !GTK_CHECK_VERSION(3, 0, 0)
static gint (*old_gtk_entry_click) (GtkWidget *, GdkEventButton *);
#endif

static void ui_paste_clipboard_cb (GtkEntry *entry)
{
  GtkEditable *edit = GTK_EDITABLE (entry);
  if (gtk_editable_get_editable(edit))
  {
    play_item_t *item = clip_get_play_item ();
    if (item)
    {
      gint pos = gtk_editable_get_position (edit);
      char *text = g_object_get_data (G_OBJECT (entry), "mrl_component");
      switch (text ? *text : 0)
      {
      default:
	text = item->mrl;
	break;
      case 'N':
	text = item->title;
	break;
      case 'T':
	text = alloca (16);
	int_to_timestring (item->start_time, text, 16);
	gtk_editable_delete_text (edit, 0, -1);
	break;
      }
      gtk_editable_insert_text (edit, text, strlen (text), &pos);
      play_item_dispose (item);
    }
    else
      old_gtk_entry_paste (entry);
  }
}

#if !GTK_CHECK_VERSION(3, 0, 0)
/* XXX this code hasn't been ported to Gtk 3 */
static gint ui_click_clipboard_cb (GtkWidget *widget, GdkEventButton *event)
{
  GtkEntry *entry = (GtkEntry *) widget;

  /* we need the same checks as GtkEntry's own button-press code */
  if (event->window != entry->text_area ||
      (entry->button && event->button != entry->button))
    return FALSE;

  /* do our own handling of middle-button clicks */
  if (event->button == 2 && event->type == GDK_BUTTON_PRESS && entry->editable)
  {
    GtkEditable *edit = GTK_EDITABLE (entry);
    play_item_t *item = clip_get_play_item ();
    if (item)
    {
      gint pos = gtk_editable_get_position (edit);
      gtk_editable_insert_text (edit, item->mrl, strlen (item->mrl), &pos);
      play_item_dispose (item);
      entry->button = event->button; /* GtkEntry does this, so we do too */
      return TRUE;
    }
    /* if we reach here, nothing has been pasted... */
  }

  return old_gtk_entry_click (widget, event);
}
#endif /* !GTK_CHECK_VERSION(3, 0, 0) */

/* UI code init */

void ui_init (void)
{
  xine_cfg_entry_t entry;
  unsigned int i;
  se_o_t *toolbars;

  static const ui_property_t tb_props[] = {
    { "fs", N_("v=bool, toggle(): full-screen toolbar visibility"), UI_BOOL (fs_toolbar, fs_toolbar_visible, NULL) },
    { "wm", N_("v=bool, toggle(): windowed-mode toolbar visibility"), UI_BOOL (wm_toolbar, wm_toolbar_visible, NULL) },
    { NULL }
  };
  static const ui_property_t tb_fs_props[] = {
    { "at_top", N_("v=bool, toggle(): full-screen toolbar position"), UI_BOOL (fs_toolbar_pos, fs_toolbar_at_top, set_fs_toolbar_top) },
    { NULL }
  };
  static const ui_property_t tb_wm_props[] = {
    { "snap", N_("v=bool, toggle(): windowed-mode toolbar attachment"), UI_BOOL (wm_toolbar_snap, wm_toolbar_snap, NULL) },
    { NULL }
  };

#ifdef WITH_DEPRECATED
  static const se_f_def_t defs[] = {
    { "toolbar_show", js_fs_toolbar_show, 0, JSFUN_FAST_NATIVE,
      SE_GROUP_HIDDEN, N_("[bool]"), NULL },
#ifdef WITH_OBSOLETE
    { "set_toolbar_position", js_fs_toolbar_position, 0, JSFUN_FAST_NATIVE,
      SE_GROUP_HIDDEN, N_("bool"), N_("at top if true") },
#endif
    { NULL }
  };

  se_defuns (gse, NULL, defs);
#endif

  /* Intercept GtkEntry's clipboard paste and button press code.
   * This should be done via events, but the paste event handler can't return a
   * value and both are useful for all GtkEntry widgets (but are only needed
   * for a couple of them).
   */
  {
    GtkEntryClass *klass = GTK_ENTRY_CLASS (g_type_class_peek (GTK_TYPE_ENTRY));
    GtkWidgetClass *wclass = (GtkWidgetClass *) klass;
    old_gtk_entry_paste = klass->paste_clipboard;
    klass->paste_clipboard = ui_paste_clipboard_cb;
#if !GTK_CHECK_VERSION(3, 0, 0)
    old_gtk_entry_click = wclass->button_press_event;
    wclass->button_press_event = ui_click_clipboard_cb;
#endif
  }

  toolbars = se_create_object (gse, NULL, "toolbar", NULL,
			       SE_GROUP_PROPERTIES, NULL);

  ui_create_properties (tb_props, toolbars, SE_TYPE_BOOL);
  ui_create_properties (tb_fs_props, se_find_object (gse, toolbars, "fs"),
			SE_TYPE_BOOL);
  ui_create_properties (tb_wm_props, se_find_object (gse, toolbars, "wm"),
			SE_TYPE_BOOL);
  ui_load_icons ();

  if (xine_config_lookup_entry (xine, "gui.fullscreen_mode.toolbar", &entry))
  {
    fs_toolbar_at_top = !(entry.num_value & 1);
    fs_toolbar_visible = (entry.num_value >> 1) & 1;
  }

  for (i = 0; i < G_N_ELEMENTS (ranges); ++i)
  {
    if (ranges[i].setting)
    {
      static const se_f_def_t defs[] = {
	{ "revert", js_control_revert, 0, JSFUN_FAST_NATIVE, SE_GROUP_HIDDEN, NULL, NULL },
	{ "reset", js_control_reset, 0, JSFUN_FAST_NATIVE, SE_GROUP_HIDDEN, NULL, NULL },
	{ NULL }
      };
      jsobjs[i] =
	se_create_object (gse, NULL, ranges[i].jspref,
			  NULL, SE_GROUP_PROPERTIES,
			  "v=int, min, max, available; revert(), reset()");
      se_defuns (gse, jsobjs[i], defs);
      se_prop_create_xine_param (gse, jsobjs[i], "v",
				 ranges[i].param, SE_TYPE_INT);
      se_prop_add_listener (gse, jsobjs[i], "v", ui_listener, (void *)&ranges[i]);
      se_prop_create_int (gse, jsobjs[i], "min", ranges[i].min, TRUE);
      se_prop_create_int (gse, jsobjs[i], "max", ranges[i].max, TRUE);
      se_prop_create_bool (gse, jsobjs[i], "available", 1, TRUE);
    }
  }
}

void ui_post_init (void)
{
  unsigned int i;
  uint32_t cap = gtk_video_get_capabilities ((GtkVideo *)gtv);
  gdk_threads_enter ();
  for (i = 0; i < G_N_ELEMENTS (ranges); ++i)
  {
    if (ranges[i].param)
      inits[i] = xine_get_param (stream, ranges[i].param);

    if (ranges[i].param && i != Control_VOLUME)
      xine_set_param (stream, ranges[i].param, starts[i]);
#ifdef VO_CAP_HUE
    switch (ranges[i].type)
    {
    case CAP_VIDEO:
      if (ranges[i].cap && !(cap & ranges[i].cap))
      {
	ui_set_list (c_adj_widgets, i, "sensitive", 0);
	se_prop_set_bool (gse, jsobjs[i], "available", 0);
      }
      break;
    default:;
    }
#endif
  }
  gdk_threads_leave ();
}

/* Undo button */

static void key_undo_cb (GtkAccelGroup *accel, GObject *obj, guint keyval,
			 GdkModifierType state)
{
  gtk_dialog_response (GTK_DIALOG (obj), GTK_RESPONSE_REJECT);
}

GtkAccelGroup *ui_add_undo_response (GtkWidget *window, GtkAccelGroup *accel)
{
  if (!accel)
  {
    accel = gtk_accel_group_new ();
    gtk_window_add_accel_group (GTK_WINDOW (window), accel);
  }
  gtk_accel_group_connect (accel, GDK_Undo, 0, 1,
			   g_cclosure_new_object (G_CALLBACK (key_undo_cb),
						  G_OBJECT (window)));
  return accel;
}
