/*
 * Copyright (C) 2003-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
 *
 * key event handler and keymap editor
 */

#include "globals.h"

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>

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

#include <xine.h>
#include <xine/xmlparser.h>

#include "key_events.h"

#include "engine.h"
#include "ui.h"
#include "utils.h"
#include "menu.h"
#include "gtkvideo.h"
#include "playlist.h"
#include "player.h"
#include "xml_widgets.h"

/*
#define LOG
*/

GtkWidget *keypad;

static gboolean        list_visible = FALSE, edit_visible = FALSE,
		       is_new_binding = FALSE;
static GtkListStore   *kb_store;
static GtkTreeModel   *kb_model;
static GtkTreeView    *tree_view;
static GtkWidget      *kb_bindings_list_dlg,
		      *kb_binding_edit_dlg, *kb_binding_desc,
		      *kb_binding_command, *kb_binding_key, *kb_binding_quote;
static GtkTextBuffer  *kb_binding_command_buf;

typedef struct {
  const gchar *desc, *cmd;
  guint        keyval;
  guint        state;
  const gchar *keyname;
  gboolean     std;
} key_binding_t;

static key_binding_t editkey;
static const char *editdesc;
static key_binding_t default_binding = { N_("New binding"), "", GDK_VoidSymbol, 0 };

static GtkTreeIter catch_key_iter;

static void delete_key_binding (GtkTreeIter *);
static gchar *kb2str (key_binding_t *kb);

#define COLUMN_DESC 0
#define COLUMN_KEY  1
#define COLUMN_DATA 2

static const key_binding_t default_bindings[] = {
  { N_("Play"), "vdr ('PLAY') || play ();", GDK_Return, 0 },
  { N_("Exit gxine"), "exit ();", GDK_q, 0 },
  { N_("Pause"), "if (!vdr ('PAUSE') && !is_live_stream ()) pause ();", GDK_space, 0 },
  { N_("Stop"), "vdr ('STOP') || stop ();", 0, 0 },
  { N_("Eject"), "eject ();", 0, 0 },
  { N_("Windowed mode"), "vo_fullscreen.v = false;", GDK_Escape, 0 },
  { N_("Fullscreen mode"), "vo_fullscreen.toggle ();", GDK_f, 0 },
  { N_("Aspect ratio"), "++vo_aspect.v;", GDK_a, 0 },
  { N_("Toggle deinterlace"), "vo_deinterlace.toggle ();", GDK_i, 0 },
  { N_("Rewind / Back 1 min"), "if (!vdr ('FASTREW') && !is_live_stream ()) play (0, get_time()-60000);", GDK_Left, 0 },
  { N_("Fast forward / Forward 1 min"), "if (!vdr ('FASTFWD') && !is_live_stream ()) play (0, get_time()+60000);", GDK_Right, 0 },
  { N_("Faster"), "if (!is_live_stream ()) ++av_speed.v;", GDK_Up, 0 },
  { N_("Slower"), "if (!is_live_stream ()) --av_speed.v;", GDK_Down, 0 },
  { N_("Up"), "input_up ();", GDK_KP_8, 0 },
  { N_("Down"), "input_down ();", GDK_KP_2, 0 },
  { N_("Left"), "input_left ();", GDK_KP_4, 0 },
  { N_("Right"), "input_right ();", GDK_KP_6, 0 },
  { N_("Select/OK"), "input_select ();", GDK_KP_Enter, 0 },
  /* Menus: 1 is the DVD root menu. All have the VDR function given in []. */
  { N_("Menu 1 [main]"), "input_menu1 ();", GDK_F1, 0 },
  { N_("Menu 2 [schedule]"), "vdr ('SCHEDULE') || input_menu2 ();", GDK_F2, 0 },
  { N_("Menu 3 [channels]"), "vdr ('CHANNELS') || input_menu3 ();", GDK_F3, 0 },
  { N_("Menu 4 [timers]"), "vdr ('TIMERS') || input_menu (4);", GDK_F4, 0 },
  { N_("Menu 5 [recordings]"), "vdr ('RECORDINGS') || input_menu (5);", GDK_F5, 0 },
  { N_("Menu 6 [setup]"), "vdr ('SETUP') || input_menu (6);", GDK_F6, 0 },
  { N_("Menu 7 [user]"), "vdr ('COMMANDS') || input_menu (7);", GDK_F7, 0 },
  { N_("Previous"), "vdr ('CHANNELMINUS') || input_previous ();", GDK_KP_Page_Up, 0 },
  { N_("Next"), "vdr ('CHANNELPLUS') || input_next ();", GDK_KP_Page_Down, 0 },
  { N_("DVD previous angle"), "event ('ANGLE_PREVIOUS');", 0, 0 },
  { N_("DVD next angle"), "event ('ANGLE_NEXT');", 0, 0 },
  { N_("1 / Play, skip first 10%"), "vdr ('1') || play (10, 0);", GDK_1, 0 },
  { N_("2 / Play, skip first 20%"), "vdr ('2') || play (20, 0);", GDK_2, 0 },
  { N_("3 / Play, skip first 30%"), "vdr ('3') || play (30, 0);", GDK_3, 0 },
  { N_("4 / Play, skip first 40%"), "vdr ('4') || play (40, 0);", GDK_4, 0 },
  { N_("5 / Play, skip first half"), "vdr ('5') || play (50, 0);", GDK_5, 0 },
  { N_("6 / Play last 40%"), "vdr ('6') || play (60, 0);", GDK_6, 0 },
  { N_("7 / Play last 30%"), "vdr ('7') || play (70, 0);", GDK_7, 0 },
  { N_("8 / Play last 20%"), "vdr ('8') || play (80, 0);", GDK_8, 0 },
  { N_("9 / Play last 10%"), "vdr ('9') || play (90, 0);", GDK_9, 0 },
  { N_("0 / Play from start"), "vdr ('0') || play (0, 0);", GDK_0, 0 },
  /* (VDR) The four coloured buttons on many remote controls */
  { N_("Red"), "vdr ('RED');", GDK_r, 0 },
  { N_("Green"), "vdr ('GREEN');", GDK_g, 0 },
  { N_("Yellow"), "vdr ('YELLOW');", GDK_y, 0 },
  { N_("Blue"), "vdr ('BLUE');", GDK_b, 0 },
  /* (VDR) Start recording the current channel */
  { N_("Record"), "vdr ('RECORD');", GDK_r, GDK_CONTROL_MASK },
  /* (VDR) Power or Standby button */
  { N_("Power"), "vdr ('POWER');", 0, 0 },
  /* (VDR) Go back to a previous menu */
  { N_("Back"), "vdr ('BACK');", GDK_BackSpace, 0 },
  /* (VDR) Audio menu */
  { N_("Audio"), "vdr ('AUDIO');", 0, 0 },
  /* (VDR) Info display */
  { N_("Info"), "vdr ('INFO');", 0, 0 },
  /* (VDR) Subtitles */
  { N_("Subtitles"), "vdr ('SUBTITLES');", 0, 0 },
  { N_("Playlist next"), "playlist_play (playlist_get_item()+1);", GDK_Page_Down, 0 },
  { N_("Playlist previous"), "playlist_play (playlist_get_item()-1);", GDK_Page_Up, 0 },
  { N_("Zoom in"), "vo_zoom.v += 5;", GDK_Z, GDK_SHIFT_MASK },
  { N_("Zoom out"), "vo_zoom.v -= 5;", GDK_z, 0 },
  { N_("Zoom 100%"), "vo_zoom.v = 100;", GDK_z, GDK_CONTROL_MASK },
  { N_("Volume +"), "if (!vdr ('VOLPLUS'))\n{\n  ao_mute.v = false;\n  ao_volume.v += 5;\n}", GDK_v, 0 },
  { N_("Volume -"), "if (!vdr ('VOLMINUS'))\n{\n  ao_mute.v = false;\n  ao_volume.v -= 5;\n}", GDK_V, GDK_SHIFT_MASK },
  { N_("Mute"), "vdr ('MUTE') || ao_mute.toggle ();", GDK_m, 0 },
  { 0 }
};

static inline const char *getdesc (const key_binding_t *binding)
{
   return binding->std ? gettext (binding->desc) : binding->desc;
}

static key_binding_t *lookup_binding (GtkTreeIter *iter)
{
  key_binding_t *k;
  GValue v = {0};

  gtk_tree_model_get_value (kb_model, iter, COLUMN_DATA, &v);
  k = g_value_peek_pointer (&v);
  g_value_unset (&v);
  return k;
}

void get_menu_bar_accel (GtkWidget *w, guint *keyval, GdkModifierType *mods)
{
  gchar *accelstr;
  g_object_get (gtk_widget_get_settings (w), "gtk-menu-bar-accel",
		&accelstr, NULL);
  if (accelstr)
  {
    gtk_accelerator_parse (accelstr, keyval, mods);
    free (accelstr);
  }
  if (!*keyval)
  {
    *keyval = GDK_F10; /* default binding */
    *mods = GDK_SHIFT_MASK;
  }
}

static gboolean is_menu_accel (GtkWidget *w, const GdkEventKey *event)
{
  guint keyval = 0;
  GdkModifierType mods;
  get_menu_bar_accel (w, &keyval, &mods);
  return event->keyval == keyval &&
	 (event->state & GXINE_MODIFIER_MASK) == (mods & GXINE_MODIFIER_MASK);
}

gboolean keypress_cb (GtkWidget *win, GdkEventKey *event, gpointer data)
{
  GtkTreeIter iter;
  guint keyval = gdk_keyval_to_lower (event->keyval);
  guint state = event->state & GXINE_MODIFIER_MASK;

  if (gtk_tree_model_get_iter_first (kb_model, &iter))
  {
    do
    {
      key_binding_t *key_binding = lookup_binding (&iter);
      if (key_binding->state == state &&
	  (key_binding->keyval == keyval ||
	   key_binding->keyval == ~event->hardware_keycode))
      {
	char *binding = kb2str (key_binding);
	asreprintf (&binding, _("key binding %s"), binding);
	/* defer execution of commands due to keypresses during startup */
	engine_queue_push (key_binding->cmd, NULL, NULL, NULL, NULL, binding);
	free (binding);
	return TRUE;
      }
    } while (gtk_tree_model_iter_next (kb_model, &iter));
  }

  /* Menu shortcuts */
  if (data &&
      (gtk_accel_groups_activate (G_OBJECT (data), event->keyval, event->state)
       || gtk_bindings_activate_event (data, event)))
    return TRUE;

  /* Windowed-mode separate toolbar (Alt-<key>, GTK menu bar binding, Menu) */
  if (win == app && menubar)
  {
    GtkWidget *toplevel = gtk_widget_get_toplevel (menubar);
    if (toplevel != app && gtk_widget_get_visible (toplevel))
    {
      event->window = gtk_widget_get_window (gtk_widget_get_toplevel (menubar));
      event->send_event = TRUE;
      gtk_main_do_event ((GdkEvent *) event);
      return TRUE;
    }
  }

  if (keyval == GDK_Menu && state == 0)
  {
    do_popup:
    gtk_menu_popup (GTK_MENU(popup_menu), NULL, NULL, NULL, NULL, 0,
		    gtk_get_current_event_time ());
    return TRUE;
  }
  else if (is_menu_accel (win, event))
  {
    GtkWidget *toplevel = gtk_widget_get_toplevel (menubar);
    if (!menubar || !gtk_widget_get_visible (toplevel == app ? menubar : toplevel))
      goto do_popup;
  }

  logprintf ("key_events: key %d not bound\n", event->keyval);

  return FALSE;
}

gboolean buttonpress_cb (GtkWidget *win, GdkEventButton *event, gpointer data)
{
  if (!(event->state & GXINE_MODIFIER_MASK))
    switch (event->button)
    {
    /* Full-screen toggle */
    case 1:
      if (event->type == GDK_2BUTTON_PRESS && !gtk_video_in_spu_button ((GtkVideo *)gtv, -1))
	gtk_action_activate (GTK_ACTION (action_items.fullscreen));
      return TRUE;
    /* Toolbar toggle (middle button) */
    case 2:
      if (gtk_video_is_fullscreen ((GtkVideo *)gtv))
	gtk_action_activate (GTK_ACTION(action_items.fs_toolbar));
      else
	gtk_action_activate (GTK_ACTION(action_items.wm_toolbar));
      return TRUE;
    }
  return FALSE;
}

gboolean buttonrelease_cb (GtkWidget *win, GdkEventButton *event, gpointer data)
{
  if (!(event->state & GXINE_MODIFIER_MASK) && popup_menu)
    switch (event->button)
    {
    case 3:
      gtk_menu_popup (GTK_MENU (popup_menu), NULL, NULL, NULL, NULL, 0,
		      event->time);
      return TRUE;
    }
  return FALSE;
}


static void close_list_window (void)
{
  list_visible = FALSE;
  gtk_widget_hide (kb_bindings_list_dlg);
}


static void close_edit_window (void)
{
  edit_visible = FALSE;
  if (is_new_binding)
    delete_key_binding (&catch_key_iter);
  gtk_widget_hide (kb_binding_edit_dlg);
}


static gboolean close_cb (GtkWidget* widget, gpointer data)
{
  close_edit_window ();
  close_list_window ();
  return TRUE;
}


static key_binding_t *find_key_binding (const gchar *desc, const gchar *cmd,
					GtkTreeIter *iter)
{
  if (!desc && !cmd)
    return NULL;

  if (gtk_tree_model_get_iter_first (kb_model, iter))
  {
    do
    {
      key_binding_t *key_binding = lookup_binding (iter);

      /* allow either to be null, in which case we match on the other rather
       * than on both
       */
      if ((!desc || !strcasecmp (key_binding->desc, desc)) &&
	  (!cmd || !strcasecmp (key_binding->cmd, cmd)))
	return key_binding;

    } while (gtk_tree_model_iter_next (kb_model, iter));
  }

  return NULL;
}

static gchar* kb2str (key_binding_t *kb)
{
  static struct {
    const gchar *shift, *ctrl, *alt, *mod2, *mod3, *mod4, *mod5;
  } text = { NULL };
  guint              l;
  gchar             *name;
  const gchar       *key;
  gchar		    *key_alloc = NULL;

  if (!text.shift)
  {
    text.shift = _("<Shift>");
    text.ctrl  = _("<Control>");
    text.alt   = _("<Alt>");
    text.mod2  = _("<Mod2>");
    text.mod3  = _("<Mod3>");
    text.mod4  = _("<Mod4>");
    text.mod5  = _("<Mod5>");
  }

  /*
   * calc length of resulting string
   */

  l = 1;
  if (kb->state & GDK_SHIFT_MASK)
    l += strlen (text.shift);
  if (kb->state & GDK_CONTROL_MASK)
    l += strlen (text.ctrl);
  if (kb->state & GDK_MOD1_MASK)
    l += strlen (text.alt);
  if (kb->state & GDK_MOD2_MASK)
    l += strlen (text.mod2);
  if (kb->state & GDK_MOD3_MASK)
    l += strlen (text.mod3);
  if (kb->state & GDK_MOD4_MASK)
    l += strlen (text.mod4);
  if (kb->state & GDK_MOD5_MASK)
    l += strlen (text.mod5);

  if (kb->keyval == GDK_VoidSymbol)
    key = _("undef"); /* undefined */
  else if (!(key = gdk_keyval_name (kb->keyval)))
    key = key_alloc = g_strdup_printf (_("unknown (%04X)"), kb->keyval);

  l += strlen (key);
  name = malloc (l);
  *name = 0;

  if (kb->state & GDK_SHIFT_MASK)
    strcat (name, text.shift);
  if (kb->state & GDK_CONTROL_MASK)
    strcat (name, text.ctrl);
  if (kb->state & GDK_MOD1_MASK)
    strcat (name, text.alt);
  if (kb->state & GDK_MOD2_MASK)
    strcat (name, text.mod2);
  if (kb->state & GDK_MOD3_MASK)
    strcat (name, text.mod3);
  if (kb->state & GDK_MOD4_MASK)
    strcat (name, text.mod4);
  if (kb->state & GDK_MOD5_MASK)
    strcat (name, text.mod5);

  strcat (name, key);

  g_free (key_alloc);
  return name;
}

static void modify_key_binding (GtkTreeIter *iter, key_binding_t *key_binding,
				const key_binding_t *new_binding)
{
  key_binding_t old_binding = *key_binding;

  if (new_binding->keyval != GDK_VoidSymbol)
  {
    key_binding->keyval = gdk_keyval_to_lower (new_binding->keyval);
    key_binding->state  = new_binding->state & GXINE_MODIFIER_MASK;
  }

  key_binding->keyname = kb2str (key_binding);
  key_binding->cmd     = strdup (new_binding->cmd);

  if (!new_binding->desc)
  {
    int i = -1;
    while (default_bindings[++i].desc)
      if (!strcasecmp (default_bindings[i].cmd, new_binding->cmd))
	break;
    key_binding->desc  = strdup (default_bindings[i].desc ? : new_binding->cmd);
  }
  else
    key_binding->desc  = strdup (new_binding->desc);

  key_binding->std = new_binding->std &
		     (!old_binding.desc ||
		      !strcmp (old_binding.desc, key_binding->desc));

  free ((char *)old_binding.keyname);
  free ((char *)old_binding.cmd);
  free ((char *)old_binding.desc);

  gtk_list_store_set (kb_store, iter,
		      COLUMN_DESC, getdesc (key_binding),
		      COLUMN_KEY, key_binding->keyname,
		      COLUMN_DATA, key_binding,
		      -1);
}


static void set_key_binding (const key_binding_t *new_binding)
{
  key_binding_t *key_binding;
  GtkTreeIter    iter;

  key_binding = new_binding->desc
		? find_key_binding (new_binding->desc, NULL, &iter)
		: find_key_binding (NULL, new_binding->cmd, &iter);

  if (!key_binding)
  {
    /* add new key binding */

    key_binding = g_malloc0 (sizeof (key_binding_t));
    key_binding->keyval = GDK_VoidSymbol;

    gtk_list_store_append (kb_store, &iter);
  }

  modify_key_binding (&iter, key_binding, new_binding);
}


static void delete_key_binding (GtkTreeIter *iter)
{
  key_binding_t *key_binding = lookup_binding (iter);
  free ((char *)key_binding->keyname);
  free ((char *)key_binding->cmd);
  free ((char *)key_binding->desc);
  gtk_list_store_remove (kb_store, iter);
  free (key_binding);
}


static void load_default_kb (void)
{
  int i = -1;
  while (default_bindings[++i].desc)
  {
    key_binding_t binding = default_bindings[i];
    binding.std = TRUE;
    set_key_binding (&binding);
  }
}


void save_key_bindings (void)
{
  gchar *fname = get_config_filename (FILE_KEYBINDINGS);
  FILE *f = open_write (fname, _("Failed to save key bindings"));

  if (f)
  {
    GtkTreeIter iter;

    fprintf (f, "<GXINEKB VERSION=\"5\">\n");

    if (gtk_tree_model_get_iter_first (kb_model, &iter))
    {
      do
      {
	key_binding_t *key_binding = lookup_binding (&iter);
	char *desc = xml_escape_string (key_binding->desc, XML_ESCAPE_NO_QUOTE);
	char *cmd = xml_escape_string (key_binding->cmd, XML_ESCAPE_NO_QUOTE);
	fprintf (f, "  <KEYBINDING>\n"
		    "    <DESCRIPTION%s>%s</DESCRIPTION>\n"
		    "    <COMMAND>%s</COMMAND>\n"
		    "    <KEYVAL>%d</KEYVAL>\n"
		    "    <STATE>%d</STATE>\n"
		    "  </KEYBINDING>\n",
		    key_binding->std ? "" : " user='yes'", desc, cmd,
		    key_binding->keyval, key_binding->state);
	free (desc);
	free (cmd);
      } while (gtk_tree_model_iter_next (kb_model, &iter));
    }

    fprintf (f, "</GXINEKB>\n");

    close_write (fname, f, _("Failed to save key bindings"));
  }

  g_free (fname);
}

typedef struct kb_upgrade_s { const char *oldcmd, *newcmd; } kb_upgrade_t;

static const char *kb_upgrade (const char *oldcmd,
			       const kb_upgrade_t *upgrade)
{
  while (upgrade->oldcmd)
  {
    if (!strcmp (oldcmd, upgrade->oldcmd))
      return upgrade->newcmd;
    ++upgrade;
  }
  return oldcmd;
}

static const kb_upgrade_t vdr_upgrade_to_0_4_0[] = {
  /* upgrade from 0.3.3 */
  { "play ();",		"vdr ('PLAY') || play ();"  },
  { "pause ();",	"if (!vdr ('PAUSE') && !is_live_stream ()) pause ();" },
  { "stop ();",		"vdr ('STOP') || stop ();" },
  { "play (0, get_time()-60000);", "if (!vdr ('FASTREW') && !is_live_stream ()) play (0, get_time()-60000);" },
  { "play (0, get_time()+60000);", "if (!vdr ('FASTFWD') && !is_live_stream ()) play (0, get_time()+60000);" },
  { "set_speed (get_speed()+1);", "if (!is_live_stream ()) ++av_speed.v;" },
  { "set_speed (get_speed()-1);", "if (!is_live_stream ()) --av_speed.v;" },
  { "input_menu2 ();",	"vdr ('SCHEDULE') || input_menu2 ();" },
  { "input_menu3 ();",	"vdr ('CHANNELS') || input_menu3 ();" },
  { "input_menu (4);",	"vdr ('TIMERS') || input_menu (4);" },
  { "input_menu (5);",	"vdr ('RECORDINGS') || input_menu (5);" },
  { "input_menu (6);",	"vdr ('SETUP') || input_menu (6);" },
  { "input_menu (7);",	"vdr ('COMMANDS') || input_menu (7);" },
  { "input_previous ();", "vdr ('CHANNELMINUS') || input_previous ();" },
  { "input_next ();",	"vdr ('CHANNELPLUS') || input_next ();" },
  { "play (10, 0);",	"vdr ('1') || play (10, 0);" },
  { "play (20, 0);",	"vdr ('2') || play (20, 0);" },
  { "play (30, 0);",	"vdr ('3') || play (30, 0);" },
  { "play (40, 0);",	"vdr ('4') || play (40, 0);" },
  { "play (50, 0);",	"vdr ('5') || play (50, 0);" },
  { "play (60, 0);",	"vdr ('6') || play (60, 0);" },
  { "play (70, 0);",	"vdr ('7') || play (70, 0);" },
  { "play (80, 0);",	"vdr ('8') || play (80, 0);" },
  { "play (90, 0);",	"vdr ('9') || play (90, 0);" },
  { "play (0, 0);",	"vdr ('0') || play (0, 0);" },
  /* fix breakage in 0.4.0-rc* */
  { "vdr ('PAUSE') || if (!is_live_stream ()) pause ();",	"if (!vdr ('PAUSE') && !is_live_stream ()) pause ();" },
  { "vdr ('FASTREW') || if (!is_live_stream ()) play (0, get_time()-60000);", "if (!vdr ('FASTREW') && !is_live_stream ()) play (0, get_time()-60000);" },
  { "vdr ('FASTFWD') || if (!is_live_stream ()) play (0, get_time()+60000);", "if (!vdr ('FASTFWD') && !is_live_stream ()) play (0, get_time()+60000);" },
  { NULL }
};
static const kb_upgrade_t vdr_upgrade_to_0_5_0[] = {
  /* upgrade from 0.4 series */
  { "set_fullscreen (0);",	"vo_fullscreen.v = false;" },
  { "set_fullscreen ();",	"vo_fullscreen.toggle ();" },
  { "set_aspect ();",		"++vo_aspect.v;" },
  { "set_deinterlace ();",	"vo_deinterlace.toggle ();" },
  { "if (!is_live_stream ()) set_speed (get_speed()+1);", "if (!is_live_stream ()) ++av_speed.v;" },
  { "if (!is_live_stream ()) set_speed (get_speed()-1);", "if (!is_live_stream ()) --av_speed.v;" },
  { "set_zoom (get_zoom()+5);",	"vo_zoom.v += 5;" },
  { "set_zoom (get_zoom()-5);",	"vo_zoom.v -= 5;" },
  { "set_zoom (100);",		"vo_zoom.v = 100;" },
  { "set_mute (0); set_volume (get_volume()+5);", "if (!vdr ('VOLPLUS'))\n{\n  ao_mute.v = false;\n  ao_volume.v += 5;\n}" },
  { "set_mute (0); set_volume (get_volume()-5);", "if (!vdr ('VOLMINUS'))\n{\n  ao_mute.v = false;\n  ao_volume.v -= 5;\n}" },
  { "set_mute ();",		"vdr ('MUTE') || ao_mute.toggle ();" },
  /* fix breakage in 0.4.999 snapshots */
  { "ao_mute.v = false; ao_volume.v += 5;", "if (!vdr ('VOLPLUS'))\n{\n ao_mute.v = false;\n  ao_volume.v += 5;\n}" },
  { "ao_mute.v = false; ao_volume.v -= 5;", "if (!vdr ('VOLMINUS'))\n{\n  ao_mute.v = false;\n  ao_volume.v -= 5;\n}" },
  { "ao_mute.toggle ();",	"vdr ('MUTE') || ao_mute.toggle ();" },
  { NULL }
};
static const kb_upgrade_t upgrade_to_0_5_11[] = {
  { "if (!vdr ('FASTREW') && !is_live_stream ()) play (0, get_time()-60000);",
    "if (!vdr ('FASTREW') && !is_live_stream ()) { var pos = has_time() ? 0 : get_pos()-1; play (pos < 0 ? 0 : pos, get_time()-60000); }" },
  { "if (!vdr ('FASTFWD') && !is_live_stream ()) play (0, get_time()+60000);",
    "if (!vdr ('FASTFWD') && !is_live_stream ()) { var pos = has_time() ? 0 : get_pos()+1; play (pos < 0 ? 0 : pos, get_time()+60000); }" },
  { NULL }
};

/* For upgrading from < 0.5.8 */
static inline const char *
xml2kb_to_std (const char *text)
{
  int i;
  for (i = 0; default_bindings[i].desc; ++i)
    if (!strcmp (text, gettext (default_bindings[i].desc)))
      return default_bindings[i].desc;
  return NULL;
}

/* For checking whether a description is standard (and translatable) */
static inline gboolean
xml2kb_is_std (const char *text)
{
  int i;
  for (i = 0; default_bindings[i].desc; ++i)
    if (!strcmp (text, default_bindings[i].desc))
      return TRUE;
  return FALSE;
}

static void xml2kb (xml_node_t *node, int version)
{
  key_binding_t binding = { 0 };

  foreach_glist (node, node)
    if (!strcasecmp (node->name, "command"))
      binding.cmd = node->data;
    else if (!strcasecmp (node->name, "description"))
    {
      if (version >= 4)
      {
	binding.std = !xml_parser_get_property (node, "user")
		      && xml2kb_is_std (node->data);
	binding.desc = node->data;
      }
      else
      {
	binding.desc = xml2kb_to_std (node->data);
	binding.std = !!binding.desc;
	if (!binding.desc)
	  binding.desc = node->data;
      }
      binding.desc = strdup (binding.desc);
    }
    else if (!strcasecmp (node->name, "keyval"))
      binding.keyval = atoi (node->data);
    else if (!strcasecmp (node->name, "state"))
      binding.state = atoi (node->data);

  if (!binding.cmd || !binding.keyval)
    return;

  if (version < 2)
    binding.cmd = kb_upgrade (binding.cmd, vdr_upgrade_to_0_4_0);
  if (version < 3)
    binding.cmd = kb_upgrade (binding.cmd, vdr_upgrade_to_0_5_0);
  if (version < 5)
    binding.cmd = kb_upgrade (binding.cmd, upgrade_to_0_5_11);

  logprintf ("key_events: desc='%s', cmd='%s', keyval=%d, state=%d\n",
	     binding.desc ? binding.desc : "", binding.cmd, binding.keyval,
	     binding.state);

  set_key_binding (&binding);
}

static void load_key_bindings (void)
{
  gchar *fname = get_config_filename (FILE_KEYBINDINGS);
  gchar *kbfile = read_entire_file (fname, NULL);
  int version = 1;

  if (kbfile)
  {
    xml_node_t *root = NULL;
    xml_parser_init_R (xml_parser_t *xml, kbfile, strlen (kbfile), XML_PARSER_CASE_INSENSITIVE);

    if (xml_parser_build_tree_R (xml, &root) >= 0)
    {
      if (!strcasecmp (root->name, "gxinekb"))
      {
	xml_property_t *prop;
	foreach_glist (prop, root->props)
	  if (!strcasecmp (prop->name, "version") && prop->value)
	  {
	    version = atoi (prop->value);
	    logprintf ("key_events: keybindings version %d\n", version);
	    break;
	  }

	if (version < 2)
	{
	  display_info (FROM_GXINE, _("Upgrading your key bindings"),
			_("Your key bindings are being upgraded. This adds:\n"
			" • Key binding descriptions\n"
			" • VDR support\n"
			"\n"
			"Some old deleted bindings may have been restored.\n"
			"You should check your bindings now.\n")
			);
	  load_default_kb ();
	}
	else if (version < 3)
	  display_info (FROM_GXINE, _("Upgrading your key bindings"), "");

	xml_node_t *node;
	foreach_glist (node, root->child)
	  if (!strcasecmp (node->name, "KEYBINDING"))
	    xml2kb (node->child, version);
      }
      else
      {
	g_printerr (_("key_events: error: %s is not a valid gxine keybindings file\n"),
		 fname);
	load_default_kb();
      }
      xml_parser_free_tree (root);
    }
    else
    {
      g_printerr (_("key_events: error: cannot load keybindings file %s (XML parsing failed)\n"),
	       fname);
      load_default_kb();
    }

    xml_parser_finalize_R (xml);
    free (kbfile);
  }
  else
  {
    if (errno != ENOENT)
      /* the file exists but we can't open it; permissions problem? */
      g_printerr (_("key_events: error: cannot open keybindings file %s\n"),
	       fname);
    load_default_kb();
  }

  g_free(fname);
}

void kb_edit_show (void)
{
  if (list_visible)
  {
    close_edit_window ();
    close_list_window ();
  }
  else
  {
    list_visible = TRUE;
    window_show (kb_bindings_list_dlg, NULL);
    gtk_tree_view_columns_autosize (tree_view);
  }
}


static void do_edit_binding (void)
{
  editkey = *lookup_binding (&catch_key_iter);
  editdesc = editkey.desc;
  editkey.desc = getdesc (&editkey);

  gtk_entry_set_text (GTK_ENTRY (kb_binding_desc), editkey.desc);
  gtk_text_buffer_set_text (kb_binding_command_buf, editkey.cmd, -1);
  gtk_entry_set_text (GTK_ENTRY (kb_binding_key), editkey.keyname);

  gchar *title = g_strdup_printf (_("Keybinding: %s"), editkey.desc);
  gtk_window_set_title (GTK_WINDOW (kb_binding_edit_dlg), title);
  g_free (title);

  window_show (kb_binding_edit_dlg, kb_bindings_list_dlg);
  gtk_widget_grab_focus (kb_binding_key);
}


static void kb_new_binding (GtkWidget *widget, gpointer data)
{

  set_key_binding (&default_binding);
  find_key_binding (default_binding.desc, default_binding.cmd, &catch_key_iter);
  do_edit_binding ();
}


static void kb_delete_binding (GtkWidget *widget, gpointer data)
{
  GtkTreeIter iter;
  GtkTreeSelection *sel =
    gtk_tree_view_get_selection (tree_view);

  if (gtk_tree_selection_get_selected (sel, NULL, &iter))
  {
    GtkTreePath *path = gtk_tree_model_get_path (kb_model, &iter);
    if (path)
    {
      gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE);
      gtk_tree_path_free (path);
    }

    if (edit_visible
	&& lookup_binding (&iter) == lookup_binding (&catch_key_iter))
      close_edit_window ();
    delete_key_binding (&iter);
  }
}


static void kb_edit_binding (GtkWidget *widget, gpointer data)
{
  GtkTreeSelection *sel =
    gtk_tree_view_get_selection (tree_view);

  if (sel && gtk_tree_selection_get_selected (sel, NULL, &catch_key_iter))
    do_edit_binding ();
}


static void kb_sig_edit_binding (GtkTreeView *tree, GtkTreeIter *iter,
				 GtkTreePath *path, gpointer data)
{
  kb_edit_binding (GTK_WIDGET (tree), data);
}


static void kb_add_defaults (GtkWidget *widget, gpointer data)
{
  if (!edit_visible)
  {
    int i = -1;
    while (default_bindings[++i].desc)
    {
      GtkTreeIter iter;
      key_binding_t binding = default_bindings[i];
      if (!find_key_binding (binding.desc, binding.cmd, &iter))
	set_key_binding (&binding);
    }
  }
}


static void kb_merge_defaults (GtkWidget *widget, gpointer data)
{
  if (!edit_visible)
    load_default_kb ();
}


static void kb_reload_keymap (GtkWidget *widget, gpointer data)
{
  if (!edit_visible)
  {
    gtk_list_store_clear (kb_store);
    load_key_bindings ();
  }
}


static void kb_reset_keymap (GtkWidget *widget, gpointer data)
{
  if (!edit_visible)
  {
    gtk_list_store_clear (kb_store);
    load_default_kb ();
  }
}


static void set_binding_quote (gboolean state)
{
  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (kb_binding_quote))
      == state)
    return;

  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (kb_binding_quote), state);
}


static gboolean bq_lock = FALSE;

static gboolean start_edit_key_cb (GtkWidget *widget, GdkEventFocus *event,
				   gpointer user_data)
{
  if (!bq_lock)
    set_binding_quote (FALSE);
  gtk_editable_select_region (GTK_EDITABLE(kb_binding_key), 0, 256);
  return TRUE;
}


static gboolean end_edit_key_cb (GtkWidget *widget, GdkEventFocus *event,
				 gpointer user_data)
{
  set_binding_quote (FALSE);
  return FALSE;
}


static gboolean do_edit_key_cb (GtkWidget *widget, GdkEventKey *event,
				gpointer user_data)
{
  gchar *name;

  if (   event->keyval == GDK_Control_L
      || event->keyval == GDK_Control_R
      || event->keyval == GDK_Meta_L
      || event->keyval == GDK_Meta_R
      || event->keyval == GDK_Alt_L
      || event->keyval == GDK_Alt_R
      || event->keyval == GDK_Super_L
      || event->keyval == GDK_Super_R
      || event->keyval == GDK_Hyper_L
      || event->keyval == GDK_Hyper_R
      || event->keyval == GDK_Shift_L
      || event->keyval == GDK_Shift_R
      || gtk_window_get_focus (GTK_WINDOW (kb_binding_edit_dlg)) != kb_binding_key)
    return FALSE;

  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (kb_binding_quote)))
#if !GTK_CHECK_VERSION(3, 0, 0)
    if (gtk_bindings_activate_event (GTK_OBJECT (widget), event) ||
#else
    if (gtk_bindings_activate_event (G_OBJECT (widget), event) ||
#endif
	gtk_window_activate_key (GTK_WINDOW (kb_binding_edit_dlg), event))
      return TRUE;

  /* Keys not allowed:
   * Shift-F10 (GTK hardwired menu popup);
   * GTK menu bar binding (default is F10);
   * Menu.
   */
  if (event->keyval == GDK_F10 &&
      (event->state & GXINE_MODIFIER_MASK) == GDK_SHIFT_MASK)
    return TRUE;
  if (event->keyval == GDK_Menu && !(event->state & GXINE_MODIFIER_MASK))
    return TRUE;

  if (is_menu_accel (widget, event))
    return TRUE;

  set_binding_quote (FALSE);

  if (event->keyval)
    editkey.keyval = gdk_keyval_to_lower (event->keyval);
  else
    editkey.keyval = ~event->hardware_keycode;
  editkey.state = event->state & GXINE_MODIFIER_MASK;

  name = kb2str (&editkey);
  gtk_entry_set_text (GTK_ENTRY (kb_binding_key), name);
  free (name);
  gtk_editable_select_region (GTK_EDITABLE (kb_binding_key), 0, 256);

  return TRUE;
}


static gboolean null_edit_key_cb (GtkWidget *widget, GdkEventKey *event,
				  gpointer user_data)
{
  return TRUE;
}


static void quote_edit_key_cb (GtkButton *widget, gpointer user_data)
{
  bq_lock = TRUE;
  gtk_widget_grab_focus (kb_binding_key);
  bq_lock = FALSE;
}


static gboolean close_edit_cb (GtkWidget* widget, gpointer data)
{
  close_edit_window ();
  return TRUE;
}


static void apply_edit (void)
{
  key_binding_t       *key_binding;
  const key_binding_t *found_binding;
  GtkTreeIter	       iter;
  GtkTextIter	       start, end;

  key_binding = lookup_binding (&catch_key_iter);

  const char *desc = gtk_entry_get_text (GTK_ENTRY (kb_binding_desc));
  /* use the original English version if we have an unmodified translation */
  editkey.desc = (editkey.std && !strcmp (desc, editkey.desc))
		 ? editdesc : desc;

  gtk_text_buffer_get_bounds (kb_binding_command_buf, &start, &end);
  editkey.cmd = gtk_text_buffer_get_text (kb_binding_command_buf, &start, &end,
					  FALSE);

  found_binding = find_key_binding (editkey.desc, NULL, &iter);
  if (!found_binding)
    found_binding = find_key_binding (NULL, editkey.cmd, &iter);

  if (found_binding && found_binding != key_binding)
  {
    GtkWidget *sel = strcasecmp (found_binding->desc, editkey.desc)
		     ? kb_binding_command : kb_binding_desc;
    gtk_editable_select_region (GTK_EDITABLE(sel), 0, 256);
  }
  else
    modify_key_binding (&catch_key_iter, key_binding, &editkey);
}

static JSBool js_xine_event (int type
#ifdef LOG
			    , const char *log
#endif
			   )
{
  xine_event_t event;
  se_log_fncall (log);
  event.type = type;
  event.data = NULL;
  event.data_length = 0;
  xine_event_send (stream, &event);
  return JS_TRUE;
}

#ifndef LOG
#define js_xine_event(T,L) (js_xine_event)((T))
#endif

#define JS_XINE_EVENT(L,T) \
  static JSBool js_##L (JSContext *cx, uintN argc, jsval *vp) \
  { \
    JSBool ret = js_xine_event (XINE_EVENT_##T, #L); \
    if (ret) { \
      JS_SET_RVAL (cx, vp, JSVAL_VOID); \
    } \
    return ret; \
  }

JS_XINE_EVENT (input_up, INPUT_UP)
JS_XINE_EVENT (input_down, INPUT_DOWN)
JS_XINE_EVENT (input_left, INPUT_LEFT)
JS_XINE_EVENT (input_right, INPUT_RIGHT)
JS_XINE_EVENT (input_select, INPUT_SELECT)

static JSBool js_input_menu (JSContext *cx, uintN argc, jsval *vp)
{
  jsval *argv = JS_ARGV (cx, vp);

  se_t *se = (se_t *) JS_GetContextPrivate(cx);
  xine_event_t event;
  int32 menu;

  se_log_fncall ("input_menu");

  se_argc_check (1, "input_menu");
  se_arg_is_int (0, "input_menu");

  JS_ValueToInt32 (cx, argv[0], &menu);

  if (menu < 1 || menu > 7)
  {
    se->print_cb (se->print_cb_data,
		  _("error: input_menu() expects an int between 1 and 7.\n"));
    return JS_TRUE;
  }

  event.type = 0;
  event.data = NULL;
  event.data_length = 0;

  event.type = XINE_EVENT_INPUT_MENU1 - 1 + menu;

  xine_event_send (stream, &event);

  return JS_TRUE;
}

JS_XINE_EVENT (input_menu1, INPUT_MENU1)
JS_XINE_EVENT (input_menu2, INPUT_MENU2)
JS_XINE_EVENT (input_menu3, INPUT_MENU3)
JS_XINE_EVENT (input_previous, INPUT_PREVIOUS)
JS_XINE_EVENT (input_next, INPUT_NEXT)

static JSBool js_keybindings_show (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall_checkinit ("keybindings_show");
  kb_edit_show ();
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

static JSBool js_keypad_show (JSContext *cx, uintN argc, jsval *vp)
{
  se_log_fncall_checkinit ("keypad_show");
  if (keypad)
  {
    if (gtk_widget_get_visible (keypad))
      gtk_widget_hide (keypad);
    else
      window_show (keypad, NULL);
  }
  JS_SET_RVAL (cx, vp, JSVAL_VOID);
  return JS_TRUE;
}

/* Xine event mapping data */

static const kb_xine_event_map_t xine_input = {
  "INPUT",
  {
    { "ANGLE_NEXT", XINE_EVENT_INPUT_ANGLE_NEXT },
    { "ANGLE_PREVIOUS", XINE_EVENT_INPUT_ANGLE_PREVIOUS },
    { "DOWN", XINE_EVENT_INPUT_DOWN },
    { "LEFT", XINE_EVENT_INPUT_LEFT },
    { "MENU1", XINE_EVENT_INPUT_MENU1 },
    { "MENU2", XINE_EVENT_INPUT_MENU2 },
    { "MENU3", XINE_EVENT_INPUT_MENU3 },
    { "MENU4", XINE_EVENT_INPUT_MENU4 },
    { "MENU5", XINE_EVENT_INPUT_MENU5 },
    { "MENU6", XINE_EVENT_INPUT_MENU6 },
    { "MENU7", XINE_EVENT_INPUT_MENU7 },
    { "NEXT", XINE_EVENT_INPUT_NEXT },
    { "NUMBER_0", XINE_EVENT_INPUT_NUMBER_0 },
    { "NUMBER_1", XINE_EVENT_INPUT_NUMBER_1 },
    { "NUMBER_10_ADD", XINE_EVENT_INPUT_NUMBER_10_ADD },
    { "NUMBER_2", XINE_EVENT_INPUT_NUMBER_2 },
    { "NUMBER_3", XINE_EVENT_INPUT_NUMBER_3 },
    { "NUMBER_4", XINE_EVENT_INPUT_NUMBER_4 },
    { "NUMBER_5", XINE_EVENT_INPUT_NUMBER_5 },
    { "NUMBER_6", XINE_EVENT_INPUT_NUMBER_6 },
    { "NUMBER_7", XINE_EVENT_INPUT_NUMBER_7 },
    { "NUMBER_8", XINE_EVENT_INPUT_NUMBER_8 },
    { "NUMBER_9", XINE_EVENT_INPUT_NUMBER_9 },
    { "PREVIOUS", XINE_EVENT_INPUT_PREVIOUS },
    { "RIGHT", XINE_EVENT_INPUT_RIGHT },
    { "SELECT", XINE_EVENT_INPUT_SELECT },
    { "UP", XINE_EVENT_INPUT_UP },
    { "0", XINE_EVENT_INPUT_NUMBER_0 },
    { "1", XINE_EVENT_INPUT_NUMBER_1 },
    { "10", XINE_EVENT_INPUT_NUMBER_10_ADD },
    { "2", XINE_EVENT_INPUT_NUMBER_2 },
    { "3", XINE_EVENT_INPUT_NUMBER_3 },
    { "4", XINE_EVENT_INPUT_NUMBER_4 },
    { "5", XINE_EVENT_INPUT_NUMBER_5 },
    { "6", XINE_EVENT_INPUT_NUMBER_6 },
    { "7", XINE_EVENT_INPUT_NUMBER_7 },
    { "8", XINE_EVENT_INPUT_NUMBER_8 },
    { "9", XINE_EVENT_INPUT_NUMBER_9 },
    { "-", -1 }, /* null event */
    { "" }
  }
};


int kb_xine_event_lookup (const kb_xine_event_map_t *map, const char *token)
{
  const char *prefix = strchr (token, '_');
  const kb_xine_event_id_t *lookup;
  char t0;

  if (!map)
    map = &xine_input;

  if (prefix && strlen (prefix) == prefix - token &&
      !strncasecmp (token, map->prefix, prefix - token))
    token = prefix + 1;

  t0 = toupper (token[0]);
  lookup = &map->id[0];

  while (lookup->name[0])
  {
    if (lookup->name[0] == t0 && !strcasecmp (lookup->name, token))
      return lookup->number;
    ++lookup;
  }

  return 0;
}

static void kb_xine_event_help (se_t *se, const kb_xine_event_map_t *map)
{
  gchar *out;
  const kb_xine_event_id_t *id;

  if (!map)
    map = &xine_input;

  out = g_strdup_printf ("%s_*:", map->prefix);
  for (id = &map->id[0]; id->name[0]; ++id)
    asreprintf (&out, "%s\n  %s", out, id->name);
  se->print_cb (se->print_cb_data, "%s", out);
  g_free (out);
}


static const kb_xine_event_map_t xine_vdr = {
  "VDR",
  {
    { "AUDIO", XINE_EVENT_VDR_AUDIO },
    { "BACK", XINE_EVENT_VDR_BACK },
    { "BLUE", XINE_EVENT_VDR_BLUE },
    { "CHANNELMINUS", XINE_EVENT_VDR_CHANNELMINUS },
    { "CHANNELPLUS", XINE_EVENT_VDR_CHANNELPLUS },
    { "CHANNELPREV", XINE_EVENT_VDR_CHANNELPREVIOUS },
    { "CHANNELS", XINE_EVENT_VDR_CHANNELS },
    { "COMMANDS", XINE_EVENT_VDR_COMMANDS },
    { "FASTFWD", XINE_EVENT_VDR_FASTFWD },
    { "FASTREW", XINE_EVENT_VDR_FASTREW },
    { "GREEN", XINE_EVENT_VDR_GREEN },
    { "INFO", XINE_EVENT_VDR_INFO },
    { "MUTE", XINE_EVENT_VDR_MUTE },
    { "PAUSE", XINE_EVENT_VDR_PAUSE },
    { "PLAY", XINE_EVENT_VDR_PLAY },
    { "POWER", XINE_EVENT_VDR_POWER },
    { "RECORD", XINE_EVENT_VDR_RECORD },
    { "RECORDINGS", XINE_EVENT_VDR_RECORDINGS },
    { "RED", XINE_EVENT_VDR_RED },
    { "SCHEDULE", XINE_EVENT_VDR_SCHEDULE },
    { "SETUP", XINE_EVENT_VDR_SETUP },
    { "STOP", XINE_EVENT_VDR_STOP },
    { "SUBTITLES", XINE_EVENT_VDR_INFO + 2 /* ..._VDR_SUBTITLES */ },
    { "TIMERS", XINE_EVENT_VDR_TIMERS },
    { "USER0", XINE_EVENT_VDR_INFO + 3 /* ..._VDR_USER0 */ },
    { "USER1", XINE_EVENT_VDR_USER1 },
    { "USER2", XINE_EVENT_VDR_USER2 },
    { "USER3", XINE_EVENT_VDR_USER3 },
    { "USER4", XINE_EVENT_VDR_USER4 },
    { "USER5", XINE_EVENT_VDR_USER5 },
    { "USER6", XINE_EVENT_VDR_USER6 },
    { "USER7", XINE_EVENT_VDR_USER7 },
    { "USER8", XINE_EVENT_VDR_USER8 },
    { "USER9", XINE_EVENT_VDR_USER9 },
    { "VOLMINUS", XINE_EVENT_VDR_VOLMINUS },
    { "VOLPLUS", XINE_EVENT_VDR_VOLPLUS },
    { "YELLOW", XINE_EVENT_VDR_YELLOW },
    { "" }
  }
};

JSBool js_event_generic (JSContext *cx, uintN argc, jsval *vp,
                         const char *func, const char *const *prefix,
			 ...)
{
  se_t *se = (se_t *) JS_GetContextPrivate(cx);
  xine_event_t event;
  JSString *str;
  char *evstr;
  va_list ap;
  const kb_xine_event_map_t *evlist;
  jsval *argv = JS_ARGV (cx, vp);

  JS_SET_RVAL (cx, vp, JSVAL_TRUE);

  se_log_fncall (func);
  se_argc_check_max (1, func);

  if (argc == 0)
  {
    va_start (ap, prefix);
    while ((evlist = va_arg (ap, const kb_xine_event_map_t *)))
      kb_xine_event_help (se, evlist);
    va_end (ap);
    kb_xine_event_help (se, NULL);
    return JS_TRUE;
  }

  se_arg_is_string (0, func);

  JS_SET_RVAL (cx, vp, JSVAL_FALSE);

  if (prefix)
  {
    const char *mrl = player_get_cur_mrl ();
    if (mrl)
    {
      while (*prefix)
      {
	if (!strncasecmp (mrl, *prefix, strlen (*prefix)))
	  break;
	++prefix;
      }
      if (!*prefix) /* no match found */
	return JS_TRUE;
    }
  }

  str = JS_ValueToString (cx, argv[0]);
  evstr = SE_JS_ENCODE_STRING (cx, str);
  event.type = 0;

  va_start (ap, prefix);
  while ((evlist = va_arg (ap, const kb_xine_event_map_t *)))
    if ((event.type = kb_xine_event_lookup (evlist, evstr)))
      break;
  va_end (ap);
  if (!event.type)
    event.type = kb_xine_event_lookup (NULL, evstr);

  if (!event.type)
  {
    SE_JS_FREE_ENCODED_STRING (cx, evstr);
    return JS_TRUE;
  }
  if (event.type != -1)
  {
    event.data = NULL;
    event.data_length = 0;
    logprintf ("js_%s: sending event %d\n", func, event.type);
    xine_event_send (stream, &event);
  }

  SE_JS_FREE_ENCODED_STRING (cx, evstr);
  JS_SET_RVAL (cx, vp, JSVAL_TRUE);
  return JS_TRUE;
}

static JSBool js_event (JSContext *cx, uintN argc, jsval *vp)
{
  return js_event_generic (cx, argc, vp, "event", NULL, NULL);
}

static JSBool js_vdr (JSContext *cx, uintN argc, jsval *vp)
{
  const char *const prefixes[] = { "vdr:/", "netvdr:/", "xvdr:/", NULL };
  return js_event_generic (cx, argc, vp, "vdr", prefixes,
			   &xine_vdr, NULL);
}

static void kb_response_cb (GtkDialog *dbox, int response, gpointer data)
{
  switch (response)
  {
  case GTK_RESPONSE_ACCEPT:
    save_key_bindings ();
    break;
  default:
    close_edit_window ();
    close_list_window ();
  }
}

static void edit_response_cb (GtkDialog *dbox, int response, gpointer data)
{
  switch (response)
  {
  case GTK_RESPONSE_REJECT:
    do_edit_binding ();
    break;
  case GTK_RESPONSE_OK:
    apply_edit (); /* and hide */
  default:
    close_edit_window ();
  }
}

void key_events_init (void)
{
  GtkWidget            *b, *scrolled_window, *hbox;
  GtkCellRenderer      *cell;
  GtkTreeViewColumn    *column;

  /*
   * init list store
   */

  kb_store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER);
  kb_model = GTK_TREE_MODEL (kb_store);

  load_key_bindings ();

  /*
   * create (for now invisible) kb_edit dialog
   */

  kb_bindings_list_dlg =
    gtk_dialog_new_with_buttons (_("Keybindings editor"), NULL, 0,
				GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
				GTK_STOCK_CLOSE, GTK_RESPONSE_DELETE_EVENT,
				NULL);
  gtk_window_set_default_size (GTK_WINDOW (kb_bindings_list_dlg), 400, 250);
  g_object_connect (G_OBJECT (kb_bindings_list_dlg),
	"signal::delete-event", G_CALLBACK (close_cb), NULL,
	"signal::response", G_CALLBACK (kb_response_cb), NULL,
	NULL);

  /* add a nice tree view widget here */

  tree_view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (kb_model));
  gtk_tree_view_set_rules_hint (tree_view, TRUE);
  gtk_tree_view_set_reorderable (tree_view, TRUE);

  cell = gtk_cell_renderer_text_new ();
  column = gtk_tree_view_column_new_with_attributes (_("Action"), cell,
						     "text", COLUMN_DESC, NULL);
  gtk_tree_view_column_set_resizable (column, TRUE);
  gtk_tree_view_append_column (tree_view, column);

  column = gtk_tree_view_column_new_with_attributes (_("Accelerator key"), cell,
						     "text", COLUMN_KEY, NULL);
  gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
  gtk_tree_view_append_column (tree_view, column);

  scrolled_window = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
				  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  gtk_container_add (GTK_CONTAINER (scrolled_window), GTK_WIDGET (tree_view));

  g_signal_connect (G_OBJECT (tree_view), "row-activated",
		    G_CALLBACK (kb_sig_edit_binding), kb_bindings_list_dlg);

  hbox = gtk_hbox_new (FALSE, 2);

  {
    static const GtkActionEntry kb_menu_data[] = {
      { "binding", NULL,                        N_("_Reload...") },
      { "reload",  GTK_STOCK_REVERT_TO_SAVED,   N_("_Reload bindings"),                NULL,           NULL,                           G_CALLBACK(kb_reload_keymap) },
      { "addnew",  GTK_STOCK_ADD,               N_("_Add new default bindings"),       NULL,           NULL,                           G_CALLBACK(kb_add_defaults) },
      { "merge",   NULL,                        N_("_Merge default bindings"),         NULL,           NULL,                           G_CALLBACK(kb_merge_defaults) },
      { "replace", GTK_STOCK_CLEAR,             N_("Replace with _default bindings"),  NULL,           NULL,                           G_CALLBACK(kb_reset_keymap) },
      { "new",     GTK_STOCK_NEW,               NULL,                                  NULL,           N_("Add a key binding"),        G_CALLBACK(kb_new_binding) },
      { "edit",    GTK_STOCK_EDIT,              NULL,                                  "<Control>E",   N_("Edit the selected item"),   G_CALLBACK(kb_edit_binding) },
      { "delete",  GTK_STOCK_DELETE,            NULL,                                  "Delete",       N_("Delete the selected item"), G_CALLBACK(kb_delete_binding) },
    };
    static const char kb_menu_structure[] =
      "<ui>"
	"<menubar>"
	  "<menu action='binding'>"
	    "<menuitem action='reload' />"
	    "<menuitem action='addnew' />"
	    "<menuitem action='merge' />"
	    "<menuitem action='replace' />"
	  "</menu>"
	"</menubar>"
	"<toolbar>"
	  "<toolitem action='new' />"
	  "<toolitem action='edit' />"
	  "<toolitem action='delete' />"
	"</toolbar>"
      "</ui>";

    GError *error;
    GtkUIManager *ui = ui_create_manager ("bindings", kb_bindings_list_dlg);
    GtkActionGroup *actions = ui_get_action_group (ui);
    gtk_action_group_add_actions (actions,
				  kb_menu_data, G_N_ELEMENTS(kb_menu_data),
				  kb_bindings_list_dlg);
    gtk_ui_manager_add_ui_from_string (ui, kb_menu_structure, -1, &error);
    gtk_box_pack_start
      (GTK_BOX(hbox), gtk_ui_manager_get_widget (ui, "/toolbar"), TRUE, TRUE, 0);
    gtk_box_pack_end
      (GTK_BOX(hbox), gtk_ui_manager_get_widget (ui, "/menubar"), FALSE, FALSE, 0);
    gtk_action_group_connect_accelerators (actions);
  }

  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (kb_bindings_list_dlg))),
                      hbox, FALSE, FALSE, 0);
  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (kb_bindings_list_dlg))),
                      scrolled_window, TRUE, TRUE, 2);

  list_visible = FALSE;

  /*
   * create kb_binding_edit dialogue
   */

  kb_binding_edit_dlg = gtk_dialog_new_with_buttons (_("Keybinding"), NULL, 0,
				GTK_STOCK_UNDO, GTK_RESPONSE_REJECT,
				GTK_STOCK_CANCEL, GTK_RESPONSE_DELETE_EVENT,
				GTK_STOCK_OK, GTK_RESPONSE_OK,
				NULL);
  gtk_dialog_set_default_response (GTK_DIALOG(kb_binding_edit_dlg),
				   GTK_RESPONSE_OK);
  gtk_window_set_default_size (GTK_WINDOW (kb_binding_edit_dlg), 400, 200);

  ui_add_undo_response (kb_binding_edit_dlg, NULL);

  b = gtk_table_new (3, 3, FALSE);

  kb_binding_desc = gtk_entry_new ();
  gtk_entry_set_max_length (GTK_ENTRY(kb_binding_desc), 32);
  add_table_row_items (b, 0, FALSE, _("Description"), kb_binding_desc, NULL);
  gtk_entry_set_activates_default (GTK_ENTRY(kb_binding_desc), TRUE);

  scrolled_window = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
				  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
				       GTK_SHADOW_IN);
  kb_binding_command_buf = gtk_text_buffer_new (NULL);
  kb_binding_command = gtk_text_view_new_with_buffer (kb_binding_command_buf);
  gtk_widget_set_name (kb_binding_command, "gxine_js_edit");
  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW(kb_binding_command),
			       GTK_WRAP_NONE);
  gtk_container_add (GTK_CONTAINER (scrolled_window), kb_binding_command);
  add_table_row_items (b, 1, TRUE, _("Commands"), scrolled_window, NULL);

  kb_binding_key = gtk_entry_new ();
  gtk_editable_set_editable (GTK_EDITABLE(kb_binding_key), FALSE);
  gtk_entry_set_activates_default (GTK_ENTRY(kb_binding_key), TRUE);
  kb_binding_quote = gtk_toggle_button_new_with_mnemonic (_("_Grab"));
  add_table_row_items (b, 2, FALSE, _("Accelerator key"),
		       kb_binding_key, kb_binding_quote, NULL);

  gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (kb_binding_edit_dlg))),
                     b);

  g_object_connect (G_OBJECT (kb_binding_edit_dlg),
	"signal::delete-event", G_CALLBACK (close_edit_cb), NULL,
	"signal::response", G_CALLBACK (edit_response_cb), NULL,
	"signal::key-press-event", G_CALLBACK (do_edit_key_cb), NULL,
	NULL);
  g_object_connect (G_OBJECT (kb_binding_key),
	"signal::focus-in-event", G_CALLBACK (start_edit_key_cb), NULL,
	"signal::focus-out-event", G_CALLBACK (end_edit_key_cb), NULL,
	"signal::key-press-event", G_CALLBACK (null_edit_key_cb), NULL,
	NULL);
  g_signal_connect (G_OBJECT (kb_binding_quote), "clicked",
		    G_CALLBACK (quote_edit_key_cb), NULL);

  /* script engine functions */

  {
    static const se_f_def_t defs[] = {
      { "input_up", js_input_up, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_down", js_input_down, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_left", js_input_left, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_right", js_input_right, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_select", js_input_select, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_menu", js_input_menu, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_INPUT, N_("int"), N_("range is 1 to 7") },
      { "input_menu1", js_input_menu1, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_menu2", js_input_menu2, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_menu3", js_input_menu3, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_previous", js_input_previous, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "input_next", js_input_next, 0, JSFUN_FAST_NATIVE, SE_GROUP_INPUT, NULL, NULL },
      { "keybindings_show", js_keybindings_show, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_DIALOGUE, NULL, NULL },
      { "keypad_show", js_keypad_show, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_DIALOGUE, NULL, NULL },
      { "event", js_event, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_INPUT, N_("string"), N_("event; returns true if sent") },
      { "vdr", js_vdr, 0, JSFUN_FAST_NATIVE,
	SE_GROUP_EXTERNAL, N_("string"), N_("VDR event; returns true if sent") },
      { NULL }
    };
    se_defuns (gse, NULL, defs);
  }

  keypad = widget_create_from_xml ("keypad.xml", "gxine-keypad", NULL, TRUE, FALSE);
  if (!keypad)
    gtk_action_set_sensitive (action_items.keypad, FALSE);
}
