/*
 * menu.c: The actual menu implementations
 *
 * See the main source file 'vdr.c' for copyright information and
 * how to reach the author.
 *
 * $Id: menu.c 1.482 2008/03/16 11:15:28 kls Exp $
 */

#include "menu.h"
#ifdef USE_WAREAGLEICON
#include "iconpatch.h"
#endif /* WAREAGLEICON */
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef USE_LIEMIKUUTIO
#include <math.h>
#endif /* LIEMIKUUTIO */
#include "channels.h"
#include "config.h"
#include "cutter.h"
#include "eitscan.h"
#include "i18n.h"
#include "interface.h"
#include "plugin.h"
#include "recording.h"
#include "remote.h"
#include "shutdown.h"
#include "sources.h"
#include "status.h"
#include "themes.h"
#include "timers.h"
#include "transfer.h"
#ifdef USE_TTXTSUBS
#include "vdrttxtsubshooks.h"
#endif /* TTXTSUBS */
#include "videodir.h"
#ifdef USE_MENUORG
#include "menuorgpatch.h"
#endif /* MENUORG */

#define MAXWAIT4EPGINFO   3 // seconds
#define MODETIMEOUT       3 // seconds
#define DISKSPACECHEK     5 // seconds between disk space checks
#define NEWTIMERLIMIT   120 // seconds until the start time of a new timer created from the Schedule menu,
                            // within which it will go directly into the "Edit timer" menu to allow
                            // further parameter settings

#define MAXRECORDCONTROLS (MAXDEVICES * MAXRECEIVERS)
#define MAXINSTANTRECTIME (24 * 60 - 1) // 23:59 hours
#define MAXWAITFORCAMMENU  10 // seconds to wait for the CAM menu to open
#define CAMMENURETYTIMEOUT  3 // seconds after which opening the CAM menu is retried
#define CAMRESPONSETIMEOUT  5 // seconds to wait for a response from a CAM
#define MINFREEDISK       300 // minimum free disk space (in MB) required to start recording
#define NODISKSPACEDELTA  300 // seconds between "Not enough disk space to start recording!" messages

#define CHNUMWIDTH  (numdigits(Channels.MaxNumber()) + 1)

// --- cFreeDiskSpace --------------------------------------------------------

#define MB_PER_MINUTE 25.75 // this is just an estimate!

class cFreeDiskSpace {
private:
  static time_t lastDiskSpaceCheck;
  static int lastFreeMB;
  static cString freeDiskSpaceString;
public:
  static bool HasChanged(bool ForceCheck = false);
  static const char *FreeDiskSpaceString(void) { HasChanged(); return freeDiskSpaceString; }
  };

time_t cFreeDiskSpace::lastDiskSpaceCheck = 0;
int cFreeDiskSpace::lastFreeMB = 0;
cString cFreeDiskSpace::freeDiskSpaceString;

cFreeDiskSpace FreeDiskSpace;

bool cFreeDiskSpace::HasChanged(bool ForceCheck)
{
  if (ForceCheck || time(NULL) - lastDiskSpaceCheck > DISKSPACECHEK) {
     int FreeMB;
     int Percent = VideoDiskSpace(&FreeMB);
     lastDiskSpaceCheck = time(NULL);
     if (ForceCheck || FreeMB != lastFreeMB) {
        int Minutes = int(double(FreeMB) / MB_PER_MINUTE);
        int Hours = Minutes / 60;
        Minutes %= 60;
        freeDiskSpaceString = cString::sprintf("%s %d%%  -  %2d:%02d %s", tr("Disk"), Percent, Hours, Minutes, tr("free"));
        lastFreeMB = FreeMB;
        return true;
        }
     }
  return false;
}

// --- cMenuEditCaItem -------------------------------------------------------

class cMenuEditCaItem : public cMenuEditIntItem {
protected:
  virtual void Set(void);
public:
  cMenuEditCaItem(const char *Name, int *Value);
  eOSState ProcessKey(eKeys Key);
  };

cMenuEditCaItem::cMenuEditCaItem(const char *Name, int *Value)
:cMenuEditIntItem(Name, Value, 0)
{
  Set();
}

void cMenuEditCaItem::Set(void)
{
  if (*value == CA_FTA)
     SetValue(tr("Free To Air"));
  else if (*value >= CA_ENCRYPTED_MIN)
     SetValue(tr("encrypted"));
  else
     cMenuEditIntItem::Set();
}

eOSState cMenuEditCaItem::ProcessKey(eKeys Key)
{
  eOSState state = cMenuEditItem::ProcessKey(Key);

  if (state == osUnknown) {
     if (NORMALKEY(Key) == kLeft && *value >= CA_ENCRYPTED_MIN)
        *value = CA_FTA;
     else
        return cMenuEditIntItem::ProcessKey(Key);
     Set();
     state = osContinue;
     }
  return state;
}

// --- cMenuEditSrcItem ------------------------------------------------------

class cMenuEditSrcItem : public cMenuEditIntItem {
private:
  const cSource *source;
protected:
  virtual void Set(void);
public:
  cMenuEditSrcItem(const char *Name, int *Value);
  eOSState ProcessKey(eKeys Key);
  };

cMenuEditSrcItem::cMenuEditSrcItem(const char *Name, int *Value)
:cMenuEditIntItem(Name, Value, 0)
{
  source = Sources.Get(*Value);
  Set();
}

void cMenuEditSrcItem::Set(void)
{
  if (source)
     SetValue(cString::sprintf("%s - %s", *cSource::ToString(source->Code()), source->Description()));
  else
     cMenuEditIntItem::Set();
}

eOSState cMenuEditSrcItem::ProcessKey(eKeys Key)
{
  eOSState state = cMenuEditItem::ProcessKey(Key);

  if (state == osUnknown) {
     if (NORMALKEY(Key) == kLeft) { // TODO might want to increase the delta if repeated quickly?
        if (source && source->Prev()) {
           source = (cSource *)source->Prev();
           *value = source->Code();
           }
        }
     else if (NORMALKEY(Key) == kRight) {
        if (source) {
           if (source->Next())
              source = (cSource *)source->Next();
           }
        else
           source = Sources.First();
        if (source)
           *value = source->Code();
        }
     else
        return state; // we don't call cMenuEditIntItem::ProcessKey(Key) here since we don't accept numerical input
     Set();
     state = osContinue;
     }
  return state;
}

// --- cMenuEditMapItem ------------------------------------------------------

class cMenuEditMapItem : public cMenuEditItem {
protected:
  int *value;
  const tChannelParameterMap *map;
  const char *zeroString;
  virtual void Set(void);
public:
  cMenuEditMapItem(const char *Name, int *Value, const tChannelParameterMap *Map, const char *ZeroString = NULL);
  virtual eOSState ProcessKey(eKeys Key);
  };

cMenuEditMapItem::cMenuEditMapItem(const char *Name, int *Value, const tChannelParameterMap *Map, const char *ZeroString)
:cMenuEditItem(Name)
{
  value = Value;
  map = Map;
  zeroString = ZeroString;
  Set();
}

void cMenuEditMapItem::Set(void)
{
  int n = MapToUser(*value, map);
  if (n == 999)
     SetValue(tr("auto"));
  else if (n == 0 && zeroString)
     SetValue(zeroString);
  else if (n >= 0) {
     char buf[16];
     snprintf(buf, sizeof(buf), "%d", n);
     SetValue(buf);
     }
  else
     SetValue("???");
}

eOSState cMenuEditMapItem::ProcessKey(eKeys Key)
{
  eOSState state = cMenuEditItem::ProcessKey(Key);

  if (state == osUnknown) {
     int newValue = *value;
     int n = DriverIndex(*value, map);
     if (NORMALKEY(Key) == kLeft) { // TODO might want to increase the delta if repeated quickly?
        if (n-- > 0)
           newValue = map[n].driverValue;
        }
     else if (NORMALKEY(Key) == kRight) {
        if (map[++n].userValue >= 0)
           newValue = map[n].driverValue;
        }
     else
        return state;
     if (newValue != *value) {
        *value = newValue;
        Set();
        }
     state = osContinue;
     }
  return state;
}

// --- cMenuEditChannel ------------------------------------------------------

class cMenuEditChannel : public cOsdMenu {
private:
  cChannel *channel;
  cChannel data;
  char name[256];
#ifdef USE_IPTV
  char pluginParam[256];
#endif /* IPTV */
  void Setup(void);
public:
  cMenuEditChannel(cChannel *Channel, bool New = false);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuEditChannel"; }
#endif /* GRAPHTFT */
  };

cMenuEditChannel::cMenuEditChannel(cChannel *Channel, bool New)
:cOsdMenu(tr("Edit channel"), 16)
{
  channel = Channel;
  if (channel) {
     data = *channel;
     if (New) {
        channel = NULL;
        data.nid = 0;
        data.tid = 0;
        data.rid = 0;
        }
     Setup();
     }
}

void cMenuEditChannel::Setup(void)
{
  int current = Current();
  char type = **cSource::ToString(data.source);
#define ST(s) if (strchr(s, type))

  Clear();

  // Parameters for all types of sources:
  strn0cpy(name, data.name, sizeof(name));
#ifdef USE_IPTV
  strn0cpy(pluginParam, data.pluginParam, sizeof(pluginParam));
#endif /* IPTV */
  Add(new cMenuEditStrItem( tr("Name"),          name, sizeof(name)));
  Add(new cMenuEditSrcItem( tr("Source"),       &data.source));
  Add(new cMenuEditIntItem( tr("Frequency"),    &data.frequency));
  Add(new cMenuEditIntItem( tr("Vpid"),         &data.vpid,  0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Ppid"),         &data.ppid,  0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Apid1"),        &data.apids[0], 0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Apid2"),        &data.apids[1], 0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Dpid1"),        &data.dpids[0], 0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Dpid2"),        &data.dpids[1], 0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Spid1"),        &data.spids[0], 0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Spid2"),        &data.spids[1], 0, 0x1FFF));
  Add(new cMenuEditIntItem( tr("Tpid"),         &data.tpid,  0, 0x1FFF));
  Add(new cMenuEditCaItem(  tr("CA"),           &data.caids[0]));
  Add(new cMenuEditIntItem( tr("Sid"),          &data.sid, 1, 0xFFFF));
  /* XXX not yet used
  Add(new cMenuEditIntItem( tr("Nid"),          &data.nid, 0));
  Add(new cMenuEditIntItem( tr("Tid"),          &data.tid, 0));
  Add(new cMenuEditIntItem( tr("Rid"),          &data.rid, 0));
  XXX*/
  // Parameters for specific types of sources:
  ST(" S ")  Add(new cMenuEditChrItem( tr("Polarization"), &data.polarization, "hvlr"));
  ST("CS ")  Add(new cMenuEditIntItem( tr("Srate"),        &data.srate));
  ST("CST")  Add(new cMenuEditMapItem( tr("Inversion"),    &data.inversion,    InversionValues, tr("off")));
  ST("CST")  Add(new cMenuEditMapItem( tr("CoderateH"),    &data.coderateH,    CoderateValues, tr("none")));
  ST("  T")  Add(new cMenuEditMapItem( tr("CoderateL"),    &data.coderateL,    CoderateValues, tr("none")));
  ST("C T")  Add(new cMenuEditMapItem( tr("Modulation"),   &data.modulation,   ModulationValues, "QPSK"));
  ST("  T")  Add(new cMenuEditMapItem( tr("Bandwidth"),    &data.bandwidth,    BandwidthValues));
  ST("  T")  Add(new cMenuEditMapItem( tr("Transmission"), &data.transmission, TransmissionValues));
  ST("  T")  Add(new cMenuEditMapItem( tr("Guard"),        &data.guard,        GuardValues));
  ST("  T")  Add(new cMenuEditMapItem( tr("Hierarchy"),    &data.hierarchy,    HierarchyValues, tr("none")));
#ifdef USE_IPTV
  ST("P  ")  Add(new cMenuEditStrItem( tr("Parameters"),    pluginParam, sizeof(pluginParam), tr(FileNameChars)));
#endif /* IPTV */

  SetCurrent(Get(current));
  Display();
}

eOSState cMenuEditChannel::ProcessKey(eKeys Key)
{
  int oldSource = data.source;
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     if (Key == kOk) {
        if (Channels.HasUniqueChannelID(&data, channel)) {
           data.name = strcpyrealloc(data.name, name);
#ifdef USE_IPTV
           data.pluginParam = strcpyrealloc(data.pluginParam, pluginParam);
#endif /* IPTV */
           if (channel) {
              *channel = data;
              isyslog("edited channel %d %s", channel->Number(), *data.ToText());
              state = osBack;
              }
           else {
              channel = new cChannel;
              *channel = data;
              Channels.Add(channel);
              Channels.ReNumber();
              isyslog("added channel %d %s", channel->Number(), *data.ToText());
              state = osUser1;
              }
           Channels.SetModified(true);
           }
        else {
           Skins.Message(mtError, tr("Channel settings are not unique!"));
           state = osContinue;
           }
        }
     }
  if (Key != kNone && (data.source & cSource::st_Mask) != (oldSource & cSource::st_Mask))
     Setup();
  return state;
}

// --- cMenuChannelItem ------------------------------------------------------

class cMenuChannelItem : public cOsdItem {
public:
  enum eChannelSortMode { csmNumber, csmName, csmProvider };
private:
  static eChannelSortMode sortMode;
  cChannel *channel;
public:
  cMenuChannelItem(cChannel *Channel);
  static void SetSortMode(eChannelSortMode SortMode) { sortMode = SortMode; }
  static void IncSortMode(void) { sortMode = eChannelSortMode((sortMode == csmProvider) ? csmNumber : sortMode + 1); }
  static eChannelSortMode SortMode(void) { return sortMode; }
  virtual int Compare(const cListObject &ListObject) const;
  virtual void Set(void);
  cChannel *Channel(void) { return channel; }
  };

cMenuChannelItem::eChannelSortMode cMenuChannelItem::sortMode = csmNumber;

cMenuChannelItem::cMenuChannelItem(cChannel *Channel)
{
  channel = Channel;
  if (channel->GroupSep())
     SetSelectable(false);
  Set();
}

int cMenuChannelItem::Compare(const cListObject &ListObject) const
{
  cMenuChannelItem *p = (cMenuChannelItem *)&ListObject;
  int r = -1;
  if (sortMode == csmProvider)
     r = strcoll(channel->Provider(), p->channel->Provider());
  if (sortMode == csmName || r == 0)
     r = strcoll(channel->Name(), p->channel->Name());
  if (sortMode == csmNumber || r == 0)
     r = channel->Number() - p->channel->Number();
  return r;
}

void cMenuChannelItem::Set(void)
{
  cString buffer;
  if (!channel->GroupSep()) {
     if (sortMode == csmProvider)
        buffer = cString::sprintf("%d\t%s - %s", channel->Number(), channel->Provider(), channel->Name());
#ifdef USE_WAREAGLEICON
     else if (Setup.WarEagleIcons) {
        if (channel->Vpid() == 1 || channel->Vpid() == 0)
           buffer = cString::sprintf("%d\t%s %-30s", channel->Number(), IsLangUtf8() ? ICON_RADIO_UTF8 : ICON_RADIO, channel->Name());
        else if (channel->Ca() == 0)
           buffer = cString::sprintf("%d\t%s %-30s", channel->Number(), IsLangUtf8() ? ICON_TV_UTF8 : ICON_TV, channel->Name());
        else
           buffer = cString::sprintf("%d\t%s %-30s", channel->Number(), IsLangUtf8() ? ICON_TV_CRYPTED_UTF8 : ICON_TV_CRYPTED, channel->Name());
        }
#endif /* WAREAGLEICON */
     else
        buffer = cString::sprintf("%d\t%s", channel->Number(), channel->Name());
     }
  else
     buffer = cString::sprintf("---\t%s ----------------------------------------------------------------", channel->Name());
  SetText(buffer);
}

// --- cMenuChannels ---------------------------------------------------------

#define CHANNELNUMBERTIMEOUT 1000 //ms

class cMenuChannels : public cOsdMenu {
private:
  int number;
  cTimeMs numberTimer;
  void Setup(void);
  cChannel *GetChannel(int Index);
  void Propagate(void);
protected:
  eOSState Number(eKeys Key);
  eOSState Switch(void);
  eOSState Edit(void);
  eOSState New(void);
  eOSState Delete(void);
  virtual void Move(int From, int To);
public:
  cMenuChannels(void);
  ~cMenuChannels();
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuChannels"; }
#endif /* GRAPHTFT */
  };

cMenuChannels::cMenuChannels(void)
:cOsdMenu(tr("Channels"), CHNUMWIDTH)
{
  number = 0;
  Setup();
  Channels.IncBeingEdited();
}

cMenuChannels::~cMenuChannels()
{
  Channels.DecBeingEdited();
}

void cMenuChannels::Setup(void)
{
  cChannel *currentChannel = GetChannel(Current());
  if (!currentChannel)
     currentChannel = Channels.GetByNumber(cDevice::CurrentChannel());
  cMenuChannelItem *currentItem = NULL;
  Clear();
  for (cChannel *channel = Channels.First(); channel; channel = Channels.Next(channel)) {
      if (!channel->GroupSep() || cMenuChannelItem::SortMode() == cMenuChannelItem::csmNumber && *channel->Name()) {
         cMenuChannelItem *item = new cMenuChannelItem(channel);
         Add(item);
         if (channel == currentChannel)
            currentItem = item;
         }
      }
  if (cMenuChannelItem::SortMode() != cMenuChannelItem::csmNumber)
     Sort();
  SetCurrent(currentItem);
  SetHelp(tr("Button$Edit"), tr("Button$New"), tr("Button$Delete"), tr("Button$Mark"));
  Display();
}

cChannel *cMenuChannels::GetChannel(int Index)
{
  cMenuChannelItem *p = (cMenuChannelItem *)Get(Index);
  return p ? (cChannel *)p->Channel() : NULL;
}

void cMenuChannels::Propagate(void)
{
  Channels.ReNumber();
  for (cMenuChannelItem *ci = (cMenuChannelItem *)First(); ci; ci = (cMenuChannelItem *)ci->Next())
      ci->Set();
  Display();
  Channels.SetModified(true);
}

eOSState cMenuChannels::Number(eKeys Key)
{
  if (HasSubMenu())
     return osContinue;
  if (numberTimer.TimedOut())
     number = 0;
  if (!number && Key == k0) {
     cMenuChannelItem::IncSortMode();
     Setup();
     }
  else {
     number = number * 10 + Key - k0;
     for (cMenuChannelItem *ci = (cMenuChannelItem *)First(); ci; ci = (cMenuChannelItem *)ci->Next()) {
         if (!ci->Channel()->GroupSep() && ci->Channel()->Number() == number) {
            SetCurrent(ci);
            Display();
            break;
            }
         }
     numberTimer.Set(CHANNELNUMBERTIMEOUT);
     }
  return osContinue;
}

eOSState cMenuChannels::Switch(void)
{
  if (HasSubMenu())
     return osContinue;
  cChannel *ch = GetChannel(Current());
  if (ch)
     return cDevice::PrimaryDevice()->SwitchChannel(ch, true) ? osEnd : osContinue;
  return osEnd;
}

eOSState cMenuChannels::Edit(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cChannel *ch = GetChannel(Current());
  if (ch)
     return AddSubMenu(new cMenuEditChannel(ch));
  return osContinue;
}

eOSState cMenuChannels::New(void)
{
  if (HasSubMenu())
     return osContinue;
  return AddSubMenu(new cMenuEditChannel(GetChannel(Current()), true));
}

eOSState cMenuChannels::Delete(void)
{
  if (!HasSubMenu() && Count() > 0) {
     int CurrentChannelNr = cDevice::CurrentChannel();
     cChannel *CurrentChannel = Channels.GetByNumber(CurrentChannelNr);
     int Index = Current();
     cChannel *channel = GetChannel(Current());
     int DeletedChannel = channel->Number();
     // Check if there is a timer using this channel:
     if (channel->HasTimer()) {
        Skins.Message(mtError, tr("Channel is being used by a timer!"));
        return osContinue;
        }
     if (Interface->Confirm(tr("Delete channel?"))) {
        if (CurrentChannel && channel == CurrentChannel) {
           int n = Channels.GetNextNormal(CurrentChannel->Index());
           if (n < 0)
              n = Channels.GetPrevNormal(CurrentChannel->Index());
           CurrentChannel = Channels.Get(n);
           CurrentChannelNr = 0; // triggers channel switch below
           }
        Channels.Del(channel);
        cOsdMenu::Del(Index);
        Propagate();
        Channels.SetModified(true);
        isyslog("channel %d deleted", DeletedChannel);
        if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
           if (!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring())
              Channels.SwitchTo(CurrentChannel->Number());
           else
              cDevice::SetCurrentChannel(CurrentChannel);
           }
        }
     }
  return osContinue;
}

void cMenuChannels::Move(int From, int To)
{
  int CurrentChannelNr = cDevice::CurrentChannel();
  cChannel *CurrentChannel = Channels.GetByNumber(CurrentChannelNr);
  cChannel *FromChannel = GetChannel(From);
  cChannel *ToChannel = GetChannel(To);
  if (FromChannel && ToChannel) {
     int FromNumber = FromChannel->Number();
     int ToNumber = ToChannel->Number();
     Channels.Move(FromChannel, ToChannel);
     cOsdMenu::Move(From, To);
     Propagate();
     Channels.SetModified(true);
     isyslog("channel %d moved to %d", FromNumber, ToNumber);
     if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
        if (!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring())
           Channels.SwitchTo(CurrentChannel->Number());
        else
           cDevice::SetCurrentChannel(CurrentChannel);
        }
     }
}

eOSState cMenuChannels::ProcessKey(eKeys Key)
{
  eOSState state = cOsdMenu::ProcessKey(Key);

  switch (state) {
    case osUser1: {
         cChannel *channel = Channels.Last();
         if (channel) {
            Add(new cMenuChannelItem(channel), true);
            return CloseSubMenu();
            }
         }
         break;
    default:
         if (state == osUnknown) {
            switch (Key) {
              case k0 ... k9:
                            return Number(Key);
              case kOk:     return Switch();
              case kRed:    return Edit();
              case kGreen:  return New();
              case kYellow: return Delete();
              case kBlue:   if (!HasSubMenu())
                               Mark();
                            break;
              default: break;
              }
            }
    }
  return state;
}

// --- cMenuText -------------------------------------------------------------

cMenuText::cMenuText(const char *Title, const char *Text, eDvbFont Font)
:cOsdMenu(Title)
{
  text = NULL;
  font = Font;
  SetText(Text);
}

cMenuText::~cMenuText()
{
  free(text);
}

void cMenuText::SetText(const char *Text)
{
  free(text);
  text = Text ? strdup(Text) : NULL;
}

void cMenuText::Display(void)
{
  cOsdMenu::Display();
  DisplayMenu()->SetText(text, font == fontFix); //XXX define control character in text to choose the font???
  if (text)
     cStatus::MsgOsdTextItem(text);
}

eOSState cMenuText::ProcessKey(eKeys Key)
{
  switch (Key) {
    case kUp|k_Repeat:
    case kUp:
    case kDown|k_Repeat:
    case kDown:
    case kLeft|k_Repeat:
    case kLeft:
    case kRight|k_Repeat:
    case kRight:
                  DisplayMenu()->Scroll(NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft, NORMALKEY(Key) == kLeft || NORMALKEY(Key) == kRight);
                  cStatus::MsgOsdTextItem(NULL, NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft);
                  return osContinue;
    default: break;
    }

  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kOk: return osBack;
       default:  state = osContinue;
       }
     }
  return state;
}

// --- cMenuEditTimer --------------------------------------------------------

cMenuEditTimer::cMenuEditTimer(cTimer *Timer, bool New)
:cOsdMenu(tr("Edit timer"), 12)
{
  firstday = NULL;
  timer = Timer;
  addIfConfirmed = New;
  if (timer) {
     data = *timer;
     if (New)
        data.SetFlags(tfActive);
     channel = data.Channel()->Number();
     Add(new cMenuEditBitItem( tr("Active"),       &data.flags, tfActive));
#ifdef USE_PINPLUGIN
     if (cOsd::pinValid) Add(new cMenuEditChanItem(tr("Channel"), &channel));
     else {
        char* buf = 0;
        asprintf(&buf, "%s\t%s", tr("Channel"), Channels.GetByNumber(channel)->Name());
        Add(new cOsdItem(buf));
        free(buf);
        }
#else
     Add(new cMenuEditChanItem(tr("Channel"),      &channel));
#endif /* PINPLUGIN */
     Add(new cMenuEditDateItem(tr("Day"),          &data.day, &data.weekdays));
     Add(new cMenuEditTimeItem(tr("Start"),        &data.start));
     Add(new cMenuEditTimeItem(tr("Stop"),         &data.stop));
     Add(new cMenuEditBitItem( tr("VPS"),          &data.flags, tfVps));
     Add(new cMenuEditIntItem( tr("Priority"),     &data.priority, 0, MAXPRIORITY));
     Add(new cMenuEditIntItem( tr("Lifetime"),     &data.lifetime, 0, MAXLIFETIME));
#ifdef USE_PINPLUGIN
     if (cOsd::pinValid || !data.fskProtection) Add(new cMenuEditBoolItem(tr("Childlock"),&data.fskProtection));
     else {
        char* buf = 0;
        asprintf(&buf, "%s\t%s", tr("Childlock"), data.fskProtection ? tr("yes") : tr("no"));
        Add(new cOsdItem(buf));
        free(buf);
        }
#endif /* PINPLUGIN */
#ifdef USE_LIEMIKUUTIO
     char* p = strrchr(data.file, '~');
     if (p) {
        p++;
        Utf8Strn0Cpy(name, p, sizeof(name));
        Utf8Strn0Cpy(path, data.file, sizeof(path));
        p = strrchr(path, '~');
        if (p)
           p[0] = 0;
        }
     else {
        Utf8Strn0Cpy(name, data.file, sizeof(name));
        Utf8Strn0Cpy(path, "", sizeof(path));
        }
     Add(new cMenuEditStrItem(tr("File"),           name, sizeof(name), tr(FileNameChars)));
     Add(new cMenuEditRecPathItem(tr("Path"),       path, sizeof(path)));
#else
     Add(new cMenuEditStrItem( tr("File"),          data.file, sizeof(data.file)));
#endif /* LIEMIKUUTIO */
     SetFirstDayItem();
     }
  Timers.IncBeingEdited();
}

cMenuEditTimer::~cMenuEditTimer()
{
  if (timer && addIfConfirmed)
     delete timer; // apparently it wasn't confirmed
  Timers.DecBeingEdited();
}

void cMenuEditTimer::SetFirstDayItem(void)
{
  if (!firstday && !data.IsSingleEvent()) {
     Add(firstday = new cMenuEditDateItem(tr("First day"), &data.day));
     Display();
     }
  else if (firstday && data.IsSingleEvent()) {
     Del(firstday->Index());
     firstday = NULL;
     Display();
     }
}

eOSState cMenuEditTimer::ProcessKey(eKeys Key)
{
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kOk:     {
                       cChannel *ch = Channels.GetByNumber(channel);
                       if (ch)
                          data.channel = ch;
                       else {
                          Skins.Message(mtError, tr("*** Invalid Channel ***"));
                          break;
                          }
#ifdef USE_LIEMIKUUTIO
                       if (strlen(path))
                          snprintf(data.file, sizeof(data.file), "%s~%s", path, name);
                       else
                          snprintf(data.file, sizeof(data.file), "%s", name);
#endif /* LIEMIKUUTIO */
                       if (!*data.file)
                          strcpy(data.file, data.Channel()->ShortName(true));
                       if (timer) {
                          if (memcmp(timer, &data, sizeof(data)) != 0)
                             *timer = data;
                          if (addIfConfirmed)
                             Timers.Add(timer);
                          timer->SetEventFromSchedule();
                          timer->Matches();
                          Timers.SetModified();
                          isyslog("timer %s %s (%s)", *timer->ToDescr(), addIfConfirmed ? "added" : "modified", timer->HasFlags(tfActive) ? "active" : "inactive");
                          addIfConfirmed = false;
                          }
                     }
                     return osBack;
       case kRed:
       case kGreen:
       case kYellow:
       case kBlue:   return osContinue;
       default: break;
       }
     }
  if (Key != kNone)
     SetFirstDayItem();
  return state;
}

#ifdef USE_TIMERCMD
// --- cMenuCommands ---------------------------------------------------------
// declaration shifted so it can be used in cMenuTimers
class cMenuCommands : public cOsdMenu {
private:
  cCommands *commands;
  char *parameters;
  eOSState Execute(void);
public:
  cMenuCommands(const char *Title, cCommands *Commands, const char *Parameters = NULL);
  virtual ~cMenuCommands();
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuCommands"; }
#endif /* GRAPHTFT - passt das so? */
  };
#endif /* TIMERCMD */

// --- cMenuTimerItem --------------------------------------------------------

class cMenuTimerItem : public cOsdItem {
private:
  cTimer *timer;
#ifdef USE_TIMERINFO
  char diskStatus;
#endif /* TIMERINFO */
public:
  cMenuTimerItem(cTimer *Timer);
#ifdef USE_TIMERINFO
  void SetDiskStatus(char DiskStatus);
#endif /* TIMERINFO */
  virtual int Compare(const cListObject &ListObject) const;
  virtual void Set(void);
  cTimer *Timer(void) { return timer; }
  };

cMenuTimerItem::cMenuTimerItem(cTimer *Timer)
{
  timer = Timer;
#ifdef USE_TIMERINFO
  diskStatus = ' ';
#endif /* TIMERINFO */
  Set();
}

int cMenuTimerItem::Compare(const cListObject &ListObject) const
{
  return timer->Compare(*((cMenuTimerItem *)&ListObject)->timer);
}

void cMenuTimerItem::Set(void)
{
  cString day, name("");
  if (timer->WeekDays())
     day = timer->PrintDay(0, timer->WeekDays(), false);
  else if (timer->Day() - time(NULL) < 28 * SECSINDAY) {
     day = itoa(timer->GetMDay(timer->Day()));
     name = WeekDayName(timer->Day());
     }
  else {
     struct tm tm_r;
     time_t Day = timer->Day();
     localtime_r(&Day, &tm_r);
     char buffer[16];
     strftime(buffer, sizeof(buffer), "%Y%m%d", &tm_r);
     day = buffer;
     }
#ifdef USE_LIEMIKUUTIO
  if (!Setup.ShowTimerStop) {
#ifdef USE_TIMERINFO
#ifdef USE_WAREAGLEICON
     SetText(cString::sprintf("%c%s\t%d\t%s%s%s\t%02d:%02d\t%s",
#else
     SetText(cString::sprintf("%c%c\t%d\t%s%s%s\t%02d:%02d\t%s",
#endif /* WAREAGLEICON */
                       diskStatus,
#else
#ifdef USE_WAREAGLEICON
     SetText(cString::sprintf("%s\t%d\t%s%s%s\t%02d:%02d\t%s",
#else
     SetText(cString::sprintf("%c\t%d\t%s%s%s\t%02d:%02d\t%s",
#endif /* WAREAGLEICON */
#endif /* TIMERINFO */
#ifdef USE_WAREAGLEICON
                       !(timer->HasFlags(tfActive)) ? " " : timer->FirstDay() ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_ARROW_UTF8 : ICON_ARROW : "!" : timer->Recording() ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_REC_UTF8 : ICON_REC : "#" : Setup.WarEagleIcons ? IsLangUtf8() ? ICON_CLOCK_UTF8 : ICON_CLOCK : ">",
#else
                       !(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>',
#endif /* WAREAGLEICON */
                       timer->Channel()->Number(),
                       *name,
                       *name && **name ? " " : "",
                       *day,
                       timer->Start() / 100,
                       timer->Start() % 100,
                       timer->File()));
     }
  else {
#endif /* LIEMIKUUTIO */
#ifdef USE_TIMERINFO
#ifdef USE_WAREAGLEICON
  SetText(cString::sprintf("%c%s\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s",
#else
  SetText(cString::sprintf("%c%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s",
#endif /* WAREAGLEICON */
                    diskStatus,
#else
#ifdef USE_WAREAGLEICON
  SetText(cString::sprintf("%s\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s",
#else
  SetText(cString::sprintf("%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s",
#endif /* WAREAGLEICON */
#endif /* TIMERINFO */
#ifdef USE_WAREAGLEICON
                    !(timer->HasFlags(tfActive)) ? " " : timer->FirstDay() ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_ARROW_UTF8 : ICON_ARROW : "!" : timer->Recording() ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_REC_UTF8 : ICON_REC : "#" : Setup.WarEagleIcons ? IsLangUtf8() ? ICON_CLOCK_UTF8 : ICON_CLOCK : ">",
#else
                    !(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>',
#endif /* WAREAGLEICON */
                    timer->Channel()->Number(),
                    *name,
                    *name && **name ? " " : "",
                    *day,
                    timer->Start() / 100,
                    timer->Start() % 100,
                    timer->Stop() / 100,
                    timer->Stop() % 100,
                    timer->File()));
#ifdef USE_LIEMIKUUTIO
     }
#endif /* LIEMIKUUTIO */
}

#ifdef USE_TIMERINFO
void cMenuTimerItem::SetDiskStatus(char DiskStatus)
{
  diskStatus = DiskStatus;
  Set();
}

// --- cTimerEntry -----------------------------------------------------------

class cTimerEntry : public cListObject {
private:
  cMenuTimerItem *item;
  const cTimer *timer;
  time_t start;
public:
  cTimerEntry(cMenuTimerItem *item) : item(item), timer(item->Timer()), start(timer->StartTime()) {}
  cTimerEntry(const cTimer *timer, time_t start) : item(NULL), timer(timer), start(start) {}
  virtual int Compare(const cListObject &ListObject) const;
  bool active(void) const { return timer->HasFlags(tfActive); }
  time_t startTime(void) const { return start; }
  int priority(void) const { return timer->Priority(); }
  int duration(void) const;
  bool repTimer(void) const { return !timer->IsSingleEvent(); }
  bool isDummy(void) const { return item == NULL; }
  const cTimer *Timer(void) const { return timer; }
  void SetDiskStatus(char DiskStatus);
  };

int cTimerEntry::Compare(const cListObject &ListObject) const
{
  cTimerEntry *entry = (cTimerEntry *)&ListObject;
  int r = startTime() - entry->startTime();
  if (r == 0)
     r = entry->priority() - priority();
  return r;
}

int cTimerEntry::duration(void) const
{
  int dur = (timer->Stop()  / 100 * 60 + timer->Stop()  % 100) -
            (timer->Start() / 100 * 60 + timer->Start() % 100);
  if (dur < 0)
     dur += 24 * 60;
  return dur;
}

void cTimerEntry::SetDiskStatus(char DiskStatus)
{
  if (item)
     item->SetDiskStatus(DiskStatus);
}
#endif /* TIMERINFO */

// --- cMenuTimers -----------------------------------------------------------

class cMenuTimers : public cOsdMenu {
private:
  int helpKeys;
  eOSState Edit(void);
  eOSState New(void);
  eOSState Delete(void);
  eOSState OnOff(void);
  eOSState Info(void);
  cTimer *CurrentTimer(void);
  void SetHelpKeys(void);
#ifdef USE_TIMERINFO
  void ActualiseDiskStatus(void);
  bool actualiseDiskStatus;
#endif /* TIMERINFO */
#ifdef USE_TIMERCMD
  eOSState Commands(eKeys Key = kNone);
#endif /* TIMERCMD */
public:
  cMenuTimers(void);
  virtual ~cMenuTimers();
#ifdef USE_TIMERINFO
  virtual void Display(void);
#endif /* TIMERINFO */
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuTimers"; }
#endif /* GRAPHTFT */
  };

cMenuTimers::cMenuTimers(void)
#ifdef USE_TIMERINFO
:cOsdMenu(tr("Timers"), 3, CHNUMWIDTH, 10, 6, 6)
#else
:cOsdMenu(tr("Timers"), 2, CHNUMWIDTH, 10, 6, 6)
#endif /* TIMERINFO */
{
  helpKeys = -1;
  for (cTimer *timer = Timers.First(); timer; timer = Timers.Next(timer)) {
      timer->SetEventFromSchedule(); // make sure the event is current
      Add(new cMenuTimerItem(timer));
      }
  Sort();
  SetCurrent(First());
  SetHelpKeys();
  Timers.IncBeingEdited();
#ifdef USE_TIMERINFO
  actualiseDiskStatus = true;
#endif /* TIMERINFO */
}

cMenuTimers::~cMenuTimers()
{
  Timers.DecBeingEdited();
}

cTimer *cMenuTimers::CurrentTimer(void)
{
  cMenuTimerItem *item = (cMenuTimerItem *)Get(Current());
  return item ? item->Timer() : NULL;
}

void cMenuTimers::SetHelpKeys(void)
{
  int NewHelpKeys = 0;
  cTimer *timer = CurrentTimer();
  if (timer) {
     if (timer->Event())
        NewHelpKeys = 2;
     else
        NewHelpKeys = 1;
     }
  if (NewHelpKeys != helpKeys) {
     helpKeys = NewHelpKeys;
     SetHelp(helpKeys > 0 ? tr("Button$On/Off") : NULL, tr("Button$New"), helpKeys > 0 ? tr("Button$Delete") : NULL, helpKeys == 2 ? tr("Button$Info") : NULL);
     }
}

eOSState cMenuTimers::OnOff(void)
{
  if (HasSubMenu())
     return osContinue;
  cTimer *timer = CurrentTimer();
  if (timer) {
     timer->OnOff();
     timer->SetEventFromSchedule();
     RefreshCurrent();
#ifdef USE_TIMERINFO
     Display();
#else
     DisplayCurrent(true);
#endif /* TIMERINFO */
     if (timer->FirstDay())
        isyslog("timer %s first day set to %s", *timer->ToDescr(), *timer->PrintFirstDay());
     else
        isyslog("timer %s %sactivated", *timer->ToDescr(), timer->HasFlags(tfActive) ? "" : "de");
     Timers.SetModified();
     }
  return osContinue;
}

eOSState cMenuTimers::Edit(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  isyslog("editing timer %s", *CurrentTimer()->ToDescr());
  return AddSubMenu(new cMenuEditTimer(CurrentTimer()));
}

eOSState cMenuTimers::New(void)
{
  if (HasSubMenu())
     return osContinue;
  return AddSubMenu(new cMenuEditTimer(new cTimer, true));
}

eOSState cMenuTimers::Delete(void)
{
  // Check if this timer is active:
  cTimer *ti = CurrentTimer();
  if (ti) {
     if (Interface->Confirm(tr("Delete timer?"))) {
        if (ti->Recording()) {
           if (Interface->Confirm(tr("Timer still recording - really delete?"))) {
              ti->Skip();
              cRecordControls::Process(time(NULL));
              }
           else
              return osContinue;
           }
        isyslog("deleting timer %s", *ti->ToDescr());
        Timers.Del(ti);
        cOsdMenu::Del(Current());
        Timers.SetModified();
        Display();
        }
     }
  return osContinue;
}

eOSState cMenuTimers::Info(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cTimer *ti = CurrentTimer();
  if (ti && ti->Event())
     return AddSubMenu(new cMenuEvent(ti->Event()));
  return osContinue;
}

#ifdef USE_TIMERCMD
#define CHECK_2PTR_NULL(x_,y_) ((x_)? ((y_)? y_:""):"")

eOSState cMenuTimers::Commands(eKeys Key)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cTimer *ti = CurrentTimer();
  if (ti) {
     const cEvent *pEvent = ti->Event();
     int iRecNumber=0;

     if (!pEvent) {
        Timers.SetEvents();
        pEvent = ti->Event();
        }
     if (pEvent) {
        // create a dummy recording to get the real filename
        cRecording *rc_dummy = new cRecording(ti, pEvent);
        Recordings.Load();
        cRecording *rc = Recordings.GetByName(rc_dummy->FileName());

        delete rc_dummy;
        if (rc)
           iRecNumber=rc->Index() + 1;
        }
     // TODO: Geht das so...?
     // Parameter format TimerNumber 'ChannelId' Start Stop 'Titel' 'Subtitel' 'file' RecNumer
     //                  1            2          3     4     5       6          7     8
     cString parameter = cString::sprintf("%d '%s' %d %d '%s' '%s' '%s' %d", ti->Index(),
                                         *ti->Channel()->GetChannelID().ToString(),
                                         (int)ti->StartTime(),
                                         (int)ti->StopTime(),
                                         CHECK_2PTR_NULL(pEvent, pEvent->Title()),
                                         CHECK_2PTR_NULL(pEvent, pEvent->ShortText()),
                                         ti->File(),
                                         iRecNumber);
     isyslog("timercmd: %s", *parameter);
     cMenuCommands *menu;
     eOSState state = AddSubMenu(menu = new cMenuCommands(tr("Timer commands"), &TimerCommands, parameter));
     if (Key != kNone)
        state = menu->ProcessKey(Key);
     return state;
     }
  return osContinue;
}
#endif /* TIMERCMD */

#ifdef USE_TIMERINFO
void cMenuTimers::ActualiseDiskStatus(void)
{
  if (!actualiseDiskStatus || !Count())
     return;

  // compute free disk space
  int freeMB, freeMinutes, runshortMinutes;
  VideoDiskSpace(&freeMB);
  freeMinutes = int(double(freeMB) * 1.1 / MB_PER_MINUTE); // overestimate by 10 percent
  runshortMinutes = freeMinutes / 5; // 20 Percent

  // fill entries list
  cTimerEntry *entry;
  cList<cTimerEntry> entries;
  for (cOsdItem *item = First(); item; item = Next(item))
      entries.Add(new cTimerEntry((cMenuTimerItem *)item));

  // search last start time
  time_t last = 0;
  for (entry = entries.First(); entry; entry = entries.Next(entry))
      last = max(entry->startTime(), last);

  // add entries for repeating timers
  for (entry = entries.First(); entry; entry = entries.Next(entry))
      if (entry->repTimer() && !entry->isDummy())
  for (time_t start = cTimer::IncDay(entry->startTime(), 1);
      start <= last;
  start = cTimer::IncDay(start, 1))
  if (entry->Timer()->DayMatches(start))
     entries.Add(new cTimerEntry(entry->Timer(), start));

  // set the disk-status
  entries.Sort();
  for (entry = entries.First(); entry; entry = entries.Next(entry)) {
      char status = ' ';
      if (entry->active()) {
         freeMinutes -= entry->duration();
         status = freeMinutes > runshortMinutes ? '+' : freeMinutes > 0 ? '~' /* ± 177 +/- */ : '-';
         }
      entry->SetDiskStatus(status);
#ifdef DEBUG_TIMER_INFO
      dsyslog("timer-info: %c | %d | %s | %s | %3d | %+5d -> %+5d",
              status,
              entry->startTime(),
              entry->active() ? "aktiv " : "n.akt.",
              entry->repTimer() ? entry->isDummy() ? "  dummy  " : "mehrmalig" : "einmalig ",
              entry->duration(),
              entry->active() ? freeMinutes + entry->duration() : freeMinutes,
              freeMinutes);
#endif
      }

  actualiseDiskStatus = false;
}

void cMenuTimers::Display(void)
{
  ActualiseDiskStatus();
  cOsdMenu::Display();
}
#endif /* TIMERINFO */

eOSState cMenuTimers::ProcessKey(eKeys Key)
{
  int TimerNumber = HasSubMenu() ? Count() : -1;
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kOk:     return Edit();
#ifdef USE_TIMERINFO
       case kRed:    actualiseDiskStatus = true;
                     state = OnOff(); break; // must go through SetHelpKeys()!
#else
       case kRed:    state = OnOff(); break; // must go through SetHelpKeys()!
#endif /* TIMERINFO */
       case kGreen:  return New();
#ifdef USE_TIMERINFO
       case kYellow: actualiseDiskStatus = true;
                     state = Delete(); break;
#else
       case kYellow: state = Delete(); break;
#endif /* TIMERINFO */
       case kInfo:
       case kBlue:   return Info();
                     break;
#ifdef USE_TIMERCMD
       case k1...k9: return Commands(Key);
       case k0:      return (TimerCommands.Count()? Commands():osContinue);
#endif /* TIMERCMD */
       default: break;
       }
     }
#ifdef USE_TIMERINFO
  if (TimerNumber >= 0 && !HasSubMenu()) {
     if (Timers.Get(TimerNumber)) // a newly created timer was confirmed with Ok
        Add(new cMenuTimerItem(Timers.Get(TimerNumber)), true);
     Sort();
     actualiseDiskStatus = true;
#else
  if (TimerNumber >= 0 && !HasSubMenu() && Timers.Get(TimerNumber)) {
     // a newly created timer was confirmed with Ok
     Add(new cMenuTimerItem(Timers.Get(TimerNumber)), true);
#endif /* TIMERINFO */
     Display();
     }
  if (Key != kNone)
     SetHelpKeys();
  return state;
}

// --- cMenuEvent ------------------------------------------------------------

cMenuEvent::cMenuEvent(const cEvent *Event, bool CanSwitch, bool Buttons)
:cOsdMenu(tr("Event"))
{
  event = Event;
  if (event) {
     cChannel *channel = Channels.GetByChannelID(event->ChannelID(), true);
     if (channel) {
        SetTitle(channel->Name());
        int TimerMatch = tmNone;
        Timers.GetMatch(event, &TimerMatch);
        if (Buttons)
           SetHelp(TimerMatch == tmFull ? tr("Button$Timer") : tr("Button$Record"), NULL, NULL, CanSwitch ? tr("Button$Switch") : NULL);
        }
     }
}

void cMenuEvent::Display(void)
{
  cOsdMenu::Display();
  DisplayMenu()->SetEvent(event);
#ifdef USE_GRAPHTFT
  cStatus::MsgOsdSetEvent(event);
#endif /* GRAPHTFT */
  if (event->Description())
     cStatus::MsgOsdTextItem(event->Description());
}

eOSState cMenuEvent::ProcessKey(eKeys Key)
{
  switch (Key) {
    case kUp|k_Repeat:
    case kUp:
    case kDown|k_Repeat:
    case kDown:
    case kLeft|k_Repeat:
    case kLeft:
    case kRight|k_Repeat:
    case kRight:
                  DisplayMenu()->Scroll(NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft, NORMALKEY(Key) == kLeft || NORMALKEY(Key) == kRight);
                  cStatus::MsgOsdTextItem(NULL, NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft);
                  return osContinue;
    case kInfo:   return osBack;
    default: break;
    }

  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kGreen:
       case kYellow: return osContinue;
       case kOk:     return osBack;
       default: break;
       }
     }
  return state;
}

// --- cMenuScheduleItem -----------------------------------------------------

class cMenuScheduleItem : public cOsdItem {
public:
  enum eScheduleSortMode { ssmAllThis, ssmThisThis, ssmThisAll, ssmAllAll }; // "which event(s) on which channel(s)"
private:
  static eScheduleSortMode sortMode;
public:
  const cEvent *event;
  const cChannel *channel;
  bool withDate;
  int timerMatch;
#ifdef USE_LIEMIKUUTIO
  bool withBar;
  cMenuScheduleItem(const cEvent *Event, cChannel *Channel = NULL, bool WithDate = false, bool WithBar = false);
#else
  cMenuScheduleItem(const cEvent *Event, cChannel *Channel = NULL, bool WithDate = false);
#endif /* LIEMIKUUTIO */
  static void SetSortMode(eScheduleSortMode SortMode) { sortMode = SortMode; }
  static void IncSortMode(void) { sortMode = eScheduleSortMode((sortMode == ssmAllAll) ? ssmAllThis : sortMode + 1); }
  static eScheduleSortMode SortMode(void) { return sortMode; }
  virtual int Compare(const cListObject &ListObject) const;
  bool Update(bool Force = false);
  };

cMenuScheduleItem::eScheduleSortMode cMenuScheduleItem::sortMode = ssmAllThis;

#ifdef USE_LIEMIKUUTIO
cMenuScheduleItem::cMenuScheduleItem(const cEvent *Event, cChannel *Channel, bool WithDate, bool WithBar)
#else
cMenuScheduleItem::cMenuScheduleItem(const cEvent *Event, cChannel *Channel, bool WithDate)
#endif /* LIEMIKUUTIO */
{
  event = Event;
  channel = Channel;
  withDate = WithDate;
  timerMatch = tmNone;
#ifdef USE_LIEMIKUUTIO
  withBar = WithBar;
#endif /* LIEMIKUUTIO */
  Update(true);
}

int cMenuScheduleItem::Compare(const cListObject &ListObject) const
{
  cMenuScheduleItem *p = (cMenuScheduleItem *)&ListObject;
  int r = -1;
  if (sortMode != ssmAllThis)
     r = strcoll(event->Title(), p->event->Title());
  if (sortMode == ssmAllThis || r == 0)
     r = event->StartTime() - p->event->StartTime();
  return r;
}

#ifdef USE_LIEMIKUUTIO
static const char * const ProgressBar[7] =
{
  "[      ]",
  "[|     ]",
  "[||    ]",
  "[|||   ]",
  "[||||  ]",
  "[||||| ]",
  "[||||||]"
};
#endif /* LIEMIKUUTIO */

#ifdef USE_WAREAGLEICON
static const char *TimerMatchChars[9] =
{
  " ", "t", "T",
  ICON_BLANK, ICON_CLOCK_UH, ICON_CLOCK,
  ICON_BLANK_UTF8, ICON_CLOCK_UH_UTF8, ICON_CLOCK_UTF8
};
#else
static const char *TimerMatchChars = " tT";
#endif /* WAREAGLEICON */

bool cMenuScheduleItem::Update(bool Force)
{
  bool result = false;
  int OldTimerMatch = timerMatch;
  Timers.GetMatch(event, &timerMatch);
  if (Force || timerMatch != OldTimerMatch) {
     cString buffer;
#ifdef USE_WAREAGLEICON
     const char *t = Setup.WarEagleIcons ? IsLangUtf8() ? TimerMatchChars[timerMatch+6] : TimerMatchChars[timerMatch+3] : TimerMatchChars[timerMatch];
     const char *v = event->Vps() && (event->Vps() - event->StartTime()) ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_VPS_UTF8 : ICON_VPS : "V" : " ";
     const char *r = event->SeenWithin(30) && event->IsRunning() ? Setup.WarEagleIcons ? IsLangUtf8() ? ICON_RUNNING_UTF8 : ICON_RUNNING : "*" : " ";
#else
     char t = TimerMatchChars[timerMatch];
     char v = event->Vps() && (event->Vps() - event->StartTime()) ? 'V' : ' ';
     char r = event->SeenWithin(30) && event->IsRunning() ? '*' : ' ';
#endif /* WAREAGLEICON */
     const char *csn = channel ? channel->ShortName(true) : NULL;
     cString eds = event->GetDateString();
     if (channel && withDate)
#ifdef USE_WAREAGLEICON
        buffer = cString::sprintf("%d\t%.*s\t%.*s\t%s\t%s%s%s\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, Utf8SymChars(eds, 6), *eds, *event->GetTimeString(), t, v, r, event->Title());
#else
        buffer = cString::sprintf("%d\t%.*s\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, Utf8SymChars(eds, 6), *eds, *event->GetTimeString(), t, v, r, event->Title());
#endif /* WAREAGLEICON */
     else if (channel)
#ifdef USE_LIEMIKUUTIO
        if (Setup.ShowProgressBar && withBar) {
           int progress = (int)roundf( (float)(time(NULL) - event->StartTime()) / (float)(event->Duration()) * 6.0 );
           if (progress < 0) progress = 0;
           else if (progress > 6) progress = 6;
#ifdef USE_WAREAGLEICON
           buffer = cString::sprintf("%d\t%.*s\t%s\t%s\t%s%s%s\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, *event->GetTimeString(), ProgressBar[progress], t, v, r, event->Title());
#else
           buffer = cString::sprintf("%d\t%.*s\t%s\t%s\t%c%c%c\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, *event->GetTimeString(), ProgressBar[progress], t, v, r, event->Title());
#endif /* WAREAGLEICON */
           }
        else
#ifdef USE_WAREAGLEICON
           buffer = cString::sprintf("%d\t%.*s\t%s\t%s%s%s\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, *event->GetTimeString(), t, v, r, event->Title());
#else
           buffer = cString::sprintf("%d\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, *event->GetTimeString(), t, v, r, event->Title());
#endif /* WAREAGLEICON */
#else
#ifdef USE_WAREAGLEICON
        buffer = cString::sprintf("%d\t%.*s\t%s\t%s%s%s\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, *event->GetTimeString(), t, v, r, event->Title());
#else
        buffer = cString::sprintf("%d\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), Utf8SymChars(csn, 6), csn, *event->GetTimeString(), t, v, r, event->Title());
#endif /* WAREAGLEICON */
#endif /* LIEMIKUUTIO */
     else
#ifdef USE_WAREAGLEICON
        buffer = cString::sprintf("%.*s\t%s\t%s%s%s\t%s", Utf8SymChars(eds, 6), *eds, *event->GetTimeString(), t, v, r, event->Title());
#else
        buffer = cString::sprintf("%.*s\t%s\t%c%c%c\t%s", Utf8SymChars(eds, 6), *eds, *event->GetTimeString(), t, v, r, event->Title());
#endif /* WAREAGLEICON */
     SetText(buffer);
     result = true;
     }
  return result;
}

// --- cMenuWhatsOn ----------------------------------------------------------

class cMenuWhatsOn : public cOsdMenu {
private:
  bool now;
  int helpKeys;
  int timerState;
  eOSState Record(void);
  eOSState Switch(void);
  static int currentChannel;
  static const cEvent *scheduleEvent;
  bool Update(void);
  void SetHelpKeys(void);
public:
  cMenuWhatsOn(const cSchedules *Schedules, bool Now, int CurrentChannelNr);
  static int CurrentChannel(void) { return currentChannel; }
  static void SetCurrentChannel(int ChannelNr) { currentChannel = ChannelNr; }
  static const cEvent *ScheduleEvent(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return now ? "MenuWhatsOnNow" : "MenuWhatsOnNext"; }
#endif /* GRAPHTFT */
  };

int cMenuWhatsOn::currentChannel = 0;
const cEvent *cMenuWhatsOn::scheduleEvent = NULL;

cMenuWhatsOn::cMenuWhatsOn(const cSchedules *Schedules, bool Now, int CurrentChannelNr)
#ifdef USE_LIEMIKUUTIO
:cOsdMenu(Now ? tr("What's on now?") : tr("What's on next?"), CHNUMWIDTH, 7, 6, 4, 4)
#else
:cOsdMenu(Now ? tr("What's on now?") : tr("What's on next?"), CHNUMWIDTH, 7, 6, 4)
#endif /* LIEMIKUUTIO */
{
  now = Now;
  helpKeys = -1;
  timerState = 0;
  Timers.Modified(timerState);
  for (cChannel *Channel = Channels.First(); Channel; Channel = Channels.Next(Channel)) {
      if (!Channel->GroupSep()) {
         const cSchedule *Schedule = Schedules->GetSchedule(Channel);
         if (Schedule) {
            const cEvent *Event = Now ? Schedule->GetPresentEvent() : Schedule->GetFollowingEvent();
            if (Event)
#ifdef USE_LIEMIKUUTIO
               Add(new cMenuScheduleItem(Event, Channel, false, Now), Channel->Number() == CurrentChannelNr);
#else
               Add(new cMenuScheduleItem(Event, Channel), Channel->Number() == CurrentChannelNr);
#endif /* LIEMIKUUTIO */
            }
         }
      }
  currentChannel = CurrentChannelNr;
  Display();
  SetHelpKeys();
}

bool cMenuWhatsOn::Update(void)
{
  bool result = false;
  if (Timers.Modified(timerState)) {
     for (cOsdItem *item = First(); item; item = Next(item)) {
         if (((cMenuScheduleItem *)item)->Update())
            result = true;
         }
     }
  return result;
}

void cMenuWhatsOn::SetHelpKeys(void)
{
  cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
  int NewHelpKeys = 0;
  if (item) {
     if (item->timerMatch == tmFull)
        NewHelpKeys = 2;
     else
        NewHelpKeys = 1;
     }
  if (NewHelpKeys != helpKeys) {
     const char *Red[] = { NULL, tr("Button$Record"), tr("Button$Timer") };
     SetHelp(Red[NewHelpKeys], now ? tr("Button$Next") : tr("Button$Now"), tr("Button$Schedule"), tr("Button$Switch"));
     helpKeys = NewHelpKeys;
     }
}

const cEvent *cMenuWhatsOn::ScheduleEvent(void)
{
  const cEvent *ei = scheduleEvent;
  scheduleEvent = NULL;
  return ei;
}

eOSState cMenuWhatsOn::Switch(void)
{
  cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
  if (item) {
     cChannel *channel = Channels.GetByChannelID(item->event->ChannelID(), true);
     if (channel && cDevice::PrimaryDevice()->SwitchChannel(channel, true))
        return osEnd;
     }
  Skins.Message(mtError, tr("Can't switch channel!"));
  return osContinue;
}

eOSState cMenuWhatsOn::Record(void)
{
  cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
  if (item) {
     if (item->timerMatch == tmFull) {
        int tm = tmNone;
        cTimer *timer = Timers.GetMatch(item->event, &tm);
        if (timer)
           return AddSubMenu(new cMenuEditTimer(timer));
        }
     cTimer *timer = new cTimer(item->event);
     cTimer *t = Timers.GetTimer(timer);
     if (t) {
        delete timer;
        timer = t;
        return AddSubMenu(new cMenuEditTimer(timer));
        }
     else {
        Timers.Add(timer);
        Timers.SetModified();
        isyslog("timer %s added (active)", *timer->ToDescr());
        if (timer->Matches(0, false, NEWTIMERLIMIT))
           return AddSubMenu(new cMenuEditTimer(timer));
        if (HasSubMenu())
           CloseSubMenu();
        if (Update())
           Display();
        SetHelpKeys();
        }
     }
  return osContinue;
}

eOSState cMenuWhatsOn::ProcessKey(eKeys Key)
{
  bool HadSubMenu = HasSubMenu();
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kRecord:
       case kRed:    return Record();
       case kYellow: state = osBack;
                     // continue with kGreen
       case kGreen:  {
                       cMenuScheduleItem *mi = (cMenuScheduleItem *)Get(Current());
                       if (mi) {
                          scheduleEvent = mi->event;
                          currentChannel = mi->channel->Number();
                          }
                     }
                     break;
       case kBlue:   return Switch();
       case kInfo:
       case kOk:     if (Count())
                        return AddSubMenu(new cMenuEvent(((cMenuScheduleItem *)Get(Current()))->event, true, true));
                     break;
       default:      break;
       }
     }
  else if (!HasSubMenu()) {
     if (HadSubMenu && Update())
        Display();
     if (Key != kNone)
        SetHelpKeys();
     }
  return state;
}

// --- cMenuSchedule ---------------------------------------------------------

class cMenuSchedule : public cOsdMenu {
private:
  cSchedulesLock schedulesLock;
  const cSchedules *schedules;
  bool now, next;
  int otherChannel;
  int helpKeys;
  int timerState;
  eOSState Number(void);
  eOSState Record(void);
  eOSState Switch(void);
  void PrepareScheduleAllThis(const cEvent *Event, const cChannel *Channel);
  void PrepareScheduleThisThis(const cEvent *Event, const cChannel *Channel);
  void PrepareScheduleThisAll(const cEvent *Event, const cChannel *Channel);
  void PrepareScheduleAllAll(const cEvent *Event, const cChannel *Channel);
  bool Update(void);
  void SetHelpKeys(void);
public:
  cMenuSchedule(void);
  virtual ~cMenuSchedule();
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSchedule"; }
#endif /* GRAPHTFT */
  };

cMenuSchedule::cMenuSchedule(void)
:cOsdMenu("")
{
  now = next = false;
  otherChannel = 0;
  helpKeys = -1;
  timerState = 0;
  Timers.Modified(timerState);
  cMenuScheduleItem::SetSortMode(cMenuScheduleItem::ssmAllThis);
  cChannel *channel = Channels.GetByNumber(cDevice::CurrentChannel());
  if (channel) {
     cMenuWhatsOn::SetCurrentChannel(channel->Number());
     schedules = cSchedules::Schedules(schedulesLock);
     PrepareScheduleAllThis(NULL, channel);
     SetHelpKeys();
     }
}

cMenuSchedule::~cMenuSchedule()
{
  cMenuWhatsOn::ScheduleEvent(); // makes sure any posted data is cleared
}

void cMenuSchedule::PrepareScheduleAllThis(const cEvent *Event, const cChannel *Channel)
{
  Clear();
  SetCols(7, 6, 4);
  SetTitle(cString::sprintf(tr("Schedule - %s"), Channel->Name()));
  if (schedules && Channel) {
     const cSchedule *Schedule = schedules->GetSchedule(Channel);
     if (Schedule) {
        const cEvent *PresentEvent = Event ? Event : Schedule->GetPresentEvent();
        time_t now = time(NULL) - Setup.EPGLinger * 60;
        for (const cEvent *ev = Schedule->Events()->First(); ev; ev = Schedule->Events()->Next(ev)) {
            if (ev->EndTime() > now || ev == PresentEvent)
               Add(new cMenuScheduleItem(ev), ev == PresentEvent);
            }
        }
     }
}

void cMenuSchedule::PrepareScheduleThisThis(const cEvent *Event, const cChannel *Channel)
{
  Clear();
  SetCols(7, 6, 4);
  SetTitle(cString::sprintf(tr("This event - %s"), Channel->Name()));
  if (schedules && Channel && Event) {
     const cSchedule *Schedule = schedules->GetSchedule(Channel);
     if (Schedule) {
        time_t now = time(NULL) - Setup.EPGLinger * 60;
        for (const cEvent *ev = Schedule->Events()->First(); ev; ev = Schedule->Events()->Next(ev)) {
            if ((ev->EndTime() > now || ev == Event) && !strcmp(ev->Title(), Event->Title()))
               Add(new cMenuScheduleItem(ev), ev == Event);
            }
        }
     }
}

void cMenuSchedule::PrepareScheduleThisAll(const cEvent *Event, const cChannel *Channel)
{
  Clear();
  SetCols(CHNUMWIDTH, 7, 7, 6, 4);
  SetTitle(tr("This event - all channels"));
  if (schedules && Event) {
     for (cChannel *ch = Channels.First(); ch; ch = Channels.Next(ch)) {
         const cSchedule *Schedule = schedules->GetSchedule(ch);
         if (Schedule) {
            time_t now = time(NULL) - Setup.EPGLinger * 60;
            for (const cEvent *ev = Schedule->Events()->First(); ev; ev = Schedule->Events()->Next(ev)) {
                if ((ev->EndTime() > now || ev == Event) && !strcmp(ev->Title(), Event->Title()))
                   Add(new cMenuScheduleItem(ev, ch, true), ev == Event && ch == Channel);
                }
            }
         }
     }
}

void cMenuSchedule::PrepareScheduleAllAll(const cEvent *Event, const cChannel *Channel)
{
  Clear();
  SetCols(CHNUMWIDTH, 7, 7, 6, 4);
  SetTitle(tr("All events - all channels"));
  if (schedules) {
     for (cChannel *ch = Channels.First(); ch; ch = Channels.Next(ch)) {
         const cSchedule *Schedule = schedules->GetSchedule(ch);
         if (Schedule) {
            time_t now = time(NULL) - Setup.EPGLinger * 60;
            for (const cEvent *ev = Schedule->Events()->First(); ev; ev = Schedule->Events()->Next(ev)) {
                if (ev->EndTime() > now || ev == Event)
                   Add(new cMenuScheduleItem(ev, ch, true), ev == Event && ch == Channel);
                }
            }
         }
     }
}

bool cMenuSchedule::Update(void)
{
  bool result = false;
  if (Timers.Modified(timerState)) {
     for (cOsdItem *item = First(); item; item = Next(item)) {
         if (((cMenuScheduleItem *)item)->Update())
            result = true;
         }
     }
  return result;
}

void cMenuSchedule::SetHelpKeys(void)
{
  cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
  int NewHelpKeys = 0;
  if (item) {
     if (item->timerMatch == tmFull)
        NewHelpKeys = 2;
     else
        NewHelpKeys = 1;
     }
  if (NewHelpKeys != helpKeys) {
     const char *Red[] = { NULL, tr("Button$Record"), tr("Button$Timer") };
     SetHelp(Red[NewHelpKeys], tr("Button$Now"), tr("Button$Next"));
     helpKeys = NewHelpKeys;
     }
}

eOSState cMenuSchedule::Number(void)
{
  cMenuScheduleItem::IncSortMode();
  cMenuScheduleItem *CurrentItem = (cMenuScheduleItem *)Get(Current());
  const cChannel *Channel = NULL;
  const cEvent *Event = NULL;
  if (CurrentItem) {
     Event = CurrentItem->event;
     Channel = Channels.GetByChannelID(Event->ChannelID(), true);
     }
  else
     Channel = Channels.GetByNumber(cDevice::CurrentChannel());
  switch (cMenuScheduleItem::SortMode()) {
    case cMenuScheduleItem::ssmAllThis:  PrepareScheduleAllThis(Event, Channel); break;
    case cMenuScheduleItem::ssmThisThis: PrepareScheduleThisThis(Event, Channel); break;
    case cMenuScheduleItem::ssmThisAll:  PrepareScheduleThisAll(Event, Channel); break;
    case cMenuScheduleItem::ssmAllAll:   PrepareScheduleAllAll(Event, Channel); break;
    }
  CurrentItem = (cMenuScheduleItem *)Get(Current());
  Sort();
  SetCurrent(CurrentItem);
  Display();
  return osContinue;
}

eOSState cMenuSchedule::Record(void)
{
  cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
  if (item) {
     if (item->timerMatch == tmFull) {
        int tm = tmNone;
        cTimer *timer = Timers.GetMatch(item->event, &tm);
        if (timer)
           return AddSubMenu(new cMenuEditTimer(timer));
        }
     cTimer *timer = new cTimer(item->event);
     cTimer *t = Timers.GetTimer(timer);
     if (t) {
        delete timer;
        timer = t;
        return AddSubMenu(new cMenuEditTimer(timer));
        }
     else {
        Timers.Add(timer);
        Timers.SetModified();
        isyslog("timer %s added (active)", *timer->ToDescr());
        if (timer->Matches(0, false, NEWTIMERLIMIT))
           return AddSubMenu(new cMenuEditTimer(timer));
        if (HasSubMenu())
           CloseSubMenu();
        if (Update())
           Display();
        SetHelpKeys();
        }
     }
  return osContinue;
}

eOSState cMenuSchedule::Switch(void)
{
  if (otherChannel) {
     if (Channels.SwitchTo(otherChannel))
        return osEnd;
     }
  Skins.Message(mtError, tr("Can't switch channel!"));
  return osContinue;
}

eOSState cMenuSchedule::ProcessKey(eKeys Key)
{
  bool HadSubMenu = HasSubMenu();
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case k0:      return Number();
       case kRecord:
       case kRed:    return Record();
       case kGreen:  if (schedules) {
                        if (!now && !next) {
                           int ChannelNr = 0;
                           if (Count()) {
                              cChannel *channel = Channels.GetByChannelID(((cMenuScheduleItem *)Get(Current()))->event->ChannelID(), true);
                              if (channel)
                                 ChannelNr = channel->Number();
                              }
                           now = true;
                           return AddSubMenu(new cMenuWhatsOn(schedules, true, ChannelNr));
                           }
                        now = !now;
                        next = !next;
                        return AddSubMenu(new cMenuWhatsOn(schedules, now, cMenuWhatsOn::CurrentChannel()));
                        }
       case kYellow: if (schedules)
                        return AddSubMenu(new cMenuWhatsOn(schedules, false, cMenuWhatsOn::CurrentChannel()));
                     break;
       case kBlue:   if (Count() && otherChannel)
                        return Switch();
                     break;
       case kInfo:
       case kOk:     if (Count())
                        return AddSubMenu(new cMenuEvent(((cMenuScheduleItem *)Get(Current()))->event, otherChannel, true));
                     break;
       default:      break;
       }
     }
  else if (!HasSubMenu()) {
     now = next = false;
     const cEvent *ei = cMenuWhatsOn::ScheduleEvent();
     if (ei) {
        cChannel *channel = Channels.GetByChannelID(ei->ChannelID(), true);
        if (channel) {
           cMenuScheduleItem::SetSortMode(cMenuScheduleItem::ssmAllThis);
           PrepareScheduleAllThis(NULL, channel);
           if (channel->Number() != cDevice::CurrentChannel()) {
              otherChannel = channel->Number();
              SetHelp(Count() ? tr("Button$Record") : NULL, tr("Button$Now"), tr("Button$Next"), tr("Button$Switch"));
              }
           Display();
           }
        }
     else if (HadSubMenu && Update())
        Display();
     if (Key != kNone)
        SetHelpKeys();
     }
  return state;
}

// --- cMenuCommands ---------------------------------------------------------

#ifndef USE_TIMERCMD
class cMenuCommands : public cOsdMenu {
private:
  cCommands *commands;
  char *parameters;
  eOSState Execute(void);
public:
  cMenuCommands(const char *Title, cCommands *Commands, const char *Parameters = NULL);
  virtual ~cMenuCommands();
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuCommands"; }
#endif /* GRAPHTFT */
  };
#endif /* TIMERCMD */

cMenuCommands::cMenuCommands(const char *Title, cCommands *Commands, const char *Parameters)
:cOsdMenu(Title)
{
  SetHasHotkeys();
  commands = Commands;
  parameters = Parameters ? strdup(Parameters) : NULL;
  for (cCommand *command = commands->First(); command; command = commands->Next(command))
      Add(new cOsdItem(hk(command->Title())));
}

cMenuCommands::~cMenuCommands()
{
  free(parameters);
}

eOSState cMenuCommands::Execute(void)
{
  cCommand *command = commands->Get(Current());
  if (command) {
     bool confirmed = true;
#ifdef USE_CMDSUBMENU
     if (command->hasChilds()) {
        AddSubMenu(new cMenuCommands(command->Title(), command->getChilds(), parameters));
        return osContinue;
        }
#endif /* CMDSUBMENU */
     if (command->Confirm())
        confirmed = Interface->Confirm(cString::sprintf("%s?", command->Title()));
     if (confirmed) {
        Skins.Message(mtStatus, cString::sprintf("%s...", command->Title()));
        const char *Result = command->Execute(parameters);
        Skins.Message(mtStatus, NULL);
        if (Result)
           return AddSubMenu(new cMenuText(command->Title(), Result, fontFix));
        return osEnd;
        }
     }
  return osContinue;
}

eOSState cMenuCommands::ProcessKey(eKeys Key)
{
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kRed:
       case kGreen:
       case kYellow:
       case kBlue:   return osContinue;
       case kOk:     return Execute();
       default:      break;
       }
     }
  return state;
}

// --- cMenuCam --------------------------------------------------------------

class cMenuCam : public cOsdMenu {
private:
  cCamSlot *camSlot;
  cCiMenu *ciMenu;
  cCiEnquiry *ciEnquiry;
  char *input;
  int offset;
  time_t lastCamExchange;
  void GenerateTitle(const char *s = NULL);
  void QueryCam(void);
  void AddMultiLineItem(const char *s);
  void Set(void);
  eOSState Select(void);
public:
  cMenuCam(cCamSlot *CamSlot);
  virtual ~cMenuCam();
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuCam"; }
#endif /* GRAPHTFT */
  };

cMenuCam::cMenuCam(cCamSlot *CamSlot)
:cOsdMenu("", 1) // tab necessary for enquiry!
{
  camSlot = CamSlot;
  ciMenu = NULL;
  ciEnquiry = NULL;
  input = NULL;
  offset = 0;
  lastCamExchange = time(NULL);
  SetNeedsFastResponse(true);
  QueryCam();
}

cMenuCam::~cMenuCam()
{
  if (ciMenu)
     ciMenu->Abort();
  delete ciMenu;
  if (ciEnquiry)
     ciEnquiry->Abort();
  delete ciEnquiry;
  free(input);
}

void cMenuCam::GenerateTitle(const char *s)
{
  SetTitle(cString::sprintf("CAM %d - %s", camSlot->SlotNumber(), (s && *s) ? s : camSlot->GetCamName()));
}

void cMenuCam::QueryCam(void)
{
  delete ciMenu;
  ciMenu = NULL;
  delete ciEnquiry;
  ciEnquiry = NULL;
  if (camSlot->HasUserIO()) {
     ciMenu = camSlot->GetMenu();
     ciEnquiry = camSlot->GetEnquiry();
     }
  Set();
}

void cMenuCam::Set(void)
{
  if (ciMenu) {
     Clear();
     free(input);
     input = NULL;
     dsyslog("CAM %d: Menu ------------------", camSlot->SlotNumber());
     offset = 0;
     SetHasHotkeys(ciMenu->Selectable());
     GenerateTitle(ciMenu->TitleText());
     dsyslog("CAM %d: '%s'", camSlot->SlotNumber(), ciMenu->TitleText());
     if (*ciMenu->SubTitleText()) {
        dsyslog("CAM %d: '%s'", camSlot->SlotNumber(), ciMenu->SubTitleText());
        AddMultiLineItem(ciMenu->SubTitleText());
        offset = Count();
        }
     for (int i = 0; i < ciMenu->NumEntries(); i++) {
         Add(new cOsdItem(hk(ciMenu->Entry(i)), osUnknown, ciMenu->Selectable()));
         dsyslog("CAM %d: '%s'", camSlot->SlotNumber(), ciMenu->Entry(i));
         }
     if (*ciMenu->BottomText()) {
        AddMultiLineItem(ciMenu->BottomText());
        dsyslog("CAM %d: '%s'", camSlot->SlotNumber(), ciMenu->BottomText());
        }
     cRemote::TriggerLastActivity();
     }
  else if (ciEnquiry) {
     Clear();
     int Length = ciEnquiry->ExpectedLength();
     free(input);
     input = MALLOC(char, Length + 1);
     *input = 0;
     GenerateTitle();
     Add(new cOsdItem(ciEnquiry->Text(), osUnknown, false));
     Add(new cOsdItem("", osUnknown, false));
     Add(new cMenuEditNumItem("", input, Length, ciEnquiry->Blind()));
     }
  Display();
}

void cMenuCam::AddMultiLineItem(const char *s)
{
  while (s && *s) {
        const char *p = strchr(s, '\n');
        int l = p ? p - s : strlen(s);
        cOsdItem *item = new cOsdItem;
        item->SetSelectable(false);
        item->SetText(strndup(s, l), false);
        Add(item);
        s = p ? p + 1 : p;
        }
}

eOSState cMenuCam::Select(void)
{
  if (ciMenu) {
     if (ciMenu->Selectable()) {
        ciMenu->Select(Current() - offset);
        dsyslog("CAM %d: select %d", camSlot->SlotNumber(), Current() - offset);
        }
     else
        ciMenu->Cancel();
     }
  else if (ciEnquiry) {
     if (ciEnquiry->ExpectedLength() < 0xFF && int(strlen(input)) != ciEnquiry->ExpectedLength()) {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), tr("Please enter %d digits!"), ciEnquiry->ExpectedLength());
        Skins.Message(mtError, buffer);
        return osContinue;
        }
     ciEnquiry->Reply(input);
     dsyslog("CAM %d: entered '%s'", camSlot->SlotNumber(), ciEnquiry->Blind() ? "****" : input);
     }
  QueryCam();
  return osContinue;
}

eOSState cMenuCam::ProcessKey(eKeys Key)
{
  if (!camSlot->HasMMI())
     return osBack;

  eOSState state = cOsdMenu::ProcessKey(Key);

  if (ciMenu || ciEnquiry) {
     lastCamExchange = time(NULL);
     if (state == osUnknown) {
        switch (Key) {
          case kOk: return Select();
          default: break;
          }
        }
     else if (state == osBack) {
        if (ciMenu)
           ciMenu->Cancel();
        if (ciEnquiry)
           ciEnquiry->Cancel();
        QueryCam();
        return osContinue;
        }
     if (ciMenu && ciMenu->HasUpdate()) {
        QueryCam();
        return osContinue;
        }
     }
  else if (time(NULL) - lastCamExchange < CAMRESPONSETIMEOUT)
     QueryCam();
  else {
     Skins.Message(mtError, tr("CAM not responding!"));
     return osBack;
     }
  return state;
}

// --- CamControl ------------------------------------------------------------

cOsdObject *CamControl(void)
{
  for (cCamSlot *CamSlot = CamSlots.First(); CamSlot; CamSlot = CamSlots.Next(CamSlot)) {
      if (CamSlot->HasUserIO())
         return new cMenuCam(CamSlot);
      }
  return NULL;
}

// --- cMenuRecording --------------------------------------------------------

class cMenuRecording : public cOsdMenu {
private:
  const cRecording *recording;
  bool withButtons;
public:
  cMenuRecording(const cRecording *Recording, bool WithButtons = false);
  virtual void Display(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuRecording"; }
#endif /* GRAPHTFT */
};

cMenuRecording::cMenuRecording(const cRecording *Recording, bool WithButtons)
:cOsdMenu(tr("Recording info"))
{
  recording = Recording;
  withButtons = WithButtons;
  if (withButtons)
     SetHelp(tr("Button$Play"), tr("Button$Rewind"));
}

void cMenuRecording::Display(void)
{
  cOsdMenu::Display();
  DisplayMenu()->SetRecording(recording);
#ifdef USE_GRAPHTFT
  cStatus::MsgOsdSetRecording(recording);
#endif /* GRAPHTFT */
  if (recording->Info()->Description())
     cStatus::MsgOsdTextItem(recording->Info()->Description());
}

eOSState cMenuRecording::ProcessKey(eKeys Key)
{
  switch (Key) {
    case kUp|k_Repeat:
    case kUp:
    case kDown|k_Repeat:
    case kDown:
    case kLeft|k_Repeat:
    case kLeft:
    case kRight|k_Repeat:
    case kRight:
                  DisplayMenu()->Scroll(NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft, NORMALKEY(Key) == kLeft || NORMALKEY(Key) == kRight);
                  cStatus::MsgOsdTextItem(NULL, NORMALKEY(Key) == kUp || NORMALKEY(Key) == kLeft);
                  return osContinue;
    case kInfo:   return osBack;
    default: break;
    }

  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     switch (Key) {
       case kRed:    if (withButtons)
                        Key = kOk; // will play the recording, even if recording commands are defined
       case kGreen:  if (!withButtons)
                        break;
                     cRemote::Put(Key, true);
                     // continue with osBack to close the info menu and process the key
       case kOk:     return osBack;
       default: break;
       }
     }
  return state;
}

// --- cMenuRecordingItem ----------------------------------------------------

class cMenuRecordingItem : public cOsdItem {
private:
  char *fileName;
  char *name;
  int totalEntries, newEntries;
public:
  cMenuRecordingItem(cRecording *Recording, int Level);
  ~cMenuRecordingItem();
  void IncrementCounter(bool New);
  const char *Name(void) { return name; }
  const char *FileName(void) { return fileName; }
  bool IsDirectory(void) { return name != NULL; }
  };

cMenuRecordingItem::cMenuRecordingItem(cRecording *Recording, int Level)
{
  fileName = strdup(Recording->FileName());
  name = NULL;
  totalEntries = newEntries = 0;
#ifdef USE_LIEMIKUUTIO
  SetText(Recording->Title('\t', true, Level, false));
#else
  SetText(Recording->Title('\t', true, Level));
#endif /* LIEMIKUUTIO */
  if (*Text() == '\t')
     name = strdup(Text() + 2); // 'Text() + 2' to skip the two '\t'
}

cMenuRecordingItem::~cMenuRecordingItem()
{
  free(fileName);
  free(name);
}

void cMenuRecordingItem::IncrementCounter(bool New)
{
  totalEntries++;
  if (New)
     newEntries++;
#ifdef USE_LIEMIKUUTIO
#ifdef USE_WAREAGLEICON
  switch (Setup.ShowRecTime + Setup.ShowRecDate + Setup.ShowRecLength) {
     case 0:
          if (Setup.WarEagleIcons)
             SetText(cString::sprintf("%s %s", IsLangUtf8() ? ICON_FOLDER_UTF8 : ICON_FOLDER, name));
          else
             SetText(cString::sprintf("%s", name));
          break;
     case 1:
          if (Setup.WarEagleIcons)
             SetText(cString::sprintf("%s %d\t%s", IsLangUtf8() ? ICON_FOLDER_UTF8 : ICON_FOLDER, totalEntries, name));
          else
             SetText(cString::sprintf("%d\t%s", totalEntries, name));
          break;
     case 2:
     default:
          if (Setup.WarEagleIcons)
             SetText(cString::sprintf("%s (%d/%d)\t%s", IsLangUtf8() ? ICON_FOLDER_UTF8 : ICON_FOLDER, totalEntries, newEntries, name));
          else
             SetText(cString::sprintf("%d\t%d\t%s", totalEntries, newEntries, name));
          break;
     case 3:
          if (Setup.WarEagleIcons)
             SetText(cString::sprintf("%s (%d/%d)\t\t%s", IsLangUtf8() ? ICON_FOLDER_UTF8 : ICON_FOLDER, totalEntries, newEntries, name));
          else
             SetText(cString::sprintf("%d\t%d\t\t%s", totalEntries, newEntries, name));
          break;
     }
#else
  switch (Setup.ShowRecTime + Setup.ShowRecDate + Setup.ShowRecLength) {
     case 0:
          SetText(cString::sprintf("%s", name));
          break;
     case 1:
          SetText(cString::sprintf("%d\t%s", totalEntries, name));
          break;
     case 2:
     default:
          SetText(cString::sprintf("%d\t%d\t%s", totalEntries, newEntries, name));
          break;
     case 3:
          SetText(cString::sprintf("%d\t%d\t\t%s", totalEntries, newEntries, name));
          break;
     }
#endif /* WAREAGLEICON */
#else
#ifdef USE_WAREAGLEICON
  if (Setup.WarEagleIcons)
     SetText(cString::sprintf("%s (%d/%d)\t%s", IsLangUtf8() ? ICON_FOLDER_UTF8 : ICON_FOLDER, totalEntries, newEntries, name));
  else
#endif /* WAREAGLEICON */
  SetText(cString::sprintf("%d\t%d\t%s", totalEntries, newEntries, name));
#endif /* LIEMIKUUTIO */
}

#ifdef USE_LIEMIKUUTIO
// --- cMenuRenameRecording --------------------------------------------------

class cMenuRenameRecording : public cOsdMenu {
private:
  int lifetime;
  int priority;
  char name[MaxFileName];
  char path[MaxFileName];
  cOsdItem *marksItem, *resumeItem;
  bool isResume, isMarks;
  cRecording *recording;
public:
  cMenuRenameRecording(cRecording *Recording);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuRenameRecording"; }
#endif /* GRAPHTFT */
};

cMenuRenameRecording::cMenuRenameRecording(cRecording *Recording)
:cOsdMenu(tr("Rename recording"), 12)
{
  cMarks marks;

  recording = Recording;
  priority = recording->priority;
  lifetime = recording->lifetime;

  char* p = strrchr(recording->Name(), '~');
  if (p) {
     p++;
     Utf8Strn0Cpy(name, p, sizeof(name));
     Utf8Strn0Cpy(path, recording->Name(), sizeof(path));
     p = strrchr(path, '~');
     if (p)
        p[0] = 0;
     }
  else {
     Utf8Strn0Cpy(name, recording->Name(), sizeof(name));
     Utf8Strn0Cpy(path, "", sizeof(path));
     }
  Add(new cMenuEditStrItem(tr("Name"),      name,     sizeof(name), tr(FileNameChars)));
  Add(new cMenuEditRecPathItem(tr("Path"),  path,     sizeof(path)                   ));
  Add(new cMenuEditIntItem(tr("Priority"), &priority, 0,            MAXPRIORITY      ));
  Add(new cMenuEditIntItem(tr("Lifetime"), &lifetime, 0,            MAXLIFETIME      ));

  Add(new cOsdItem("", osUnknown, false));

  Add(new cOsdItem(cString::sprintf("%s:\t%s", tr("Date"), *DayDateTime(recording->start)), osUnknown, false));

  cChannel *channel = Channels.GetByChannelID(((cRecordingInfo *)recording->Info())->ChannelID());
  if (channel)
     Add(new cOsdItem(cString::sprintf("%s:\t%s", tr("Channel"), *ChannelString(channel, 0)), osUnknown, false));

  cIndexFile *index = new cIndexFile(recording->FileName(), false);
  int recLen = 0;
  if (index) {
     recLen = index->Last() / FRAMESPERSEC;
     Add(new cOsdItem(cString::sprintf("%s:\t%s", tr("Length"), *IndexToHMSF(index->Last())), osUnknown, false));
     delete index;
     }

  int dirSize = DirSizeMB(recording->FileName());
  cString bitRate = recLen ? cString::sprintf(" (%.2f MBit/s)", 8.0 * dirSize / recLen) : cString("");
  Add(new cOsdItem((dirSize > 9999) ? cString::sprintf("%s:\t%.2f GB%s", tr("Size"), dirSize / 1024.0, *bitRate) : cString::sprintf("%s:\t%d MB%s", tr("Size"), dirSize, *bitRate), osUnknown, false));

  Add(new cOsdItem("", osUnknown, false));

  isMarks = marks.Load(recording->FileName()) && marks.Count();
  marksItem = new cOsdItem(tr("Delete marks information?"), osUser1, isMarks);
  Add(marksItem);

  cResumeFile ResumeFile(recording->FileName());
  isResume = (ResumeFile.Read() != -1);
  resumeItem = new cOsdItem(tr("Delete resume information?"), osUser2, isResume);
  Add(resumeItem);
}

eOSState cMenuRenameRecording::ProcessKey(eKeys Key)
{
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
     if (Key == kOk) {
        char buffer[MaxFileName];
        if (Utf8StrLen(path))
           snprintf(buffer, sizeof(buffer), "%s~%s", path, name);
        else
           snprintf(buffer, sizeof(buffer), "%s", name);
        if (recording->Rename(buffer, &priority, &lifetime)) {
           Recordings.ChangeState();
           Recordings.TouchUpdate();
           return osRecordings;
           }
        else
           Skins.Message(mtError, tr("Error while accessing recording!"));
        }
     return osContinue;
     }
  else if (state == osUser1) {
     if (isMarks && Interface->Confirm(tr("Delete marks information?"))) {
        cMarks marks;
        marks.Load(recording->FileName());
        cMark *mark = marks.First();
        while (mark) {
          cMark *nextmark = marks.Next(mark);
          marks.Del(mark);
          mark = nextmark;
          }
        marks.Save();
        isMarks = false;
        marksItem->SetSelectable(isMarks);
        SetCurrent(First());
        Display();
        }
     return osContinue;
     }
  else if (state == osUser2) {
     if (isResume && Interface->Confirm(tr("Delete resume information?"))) {
        cResumeFile ResumeFile(recording->FileName());
        ResumeFile.Delete();
        isResume = false;
        resumeItem->SetSelectable(isResume);
        SetCurrent(First());
        Display();
        }
     return osContinue;
     }
  return state;
}
#endif /* LIEMIKUUTIO */

// --- cMenuRecordings -------------------------------------------------------

cMenuRecordings::cMenuRecordings(const char *Base, int Level, bool OpenSubMenus)
#ifdef USE_LIEMIKUUTIO
:cOsdMenu(Base ? Base : tr("Recordings"), 9, 7, 7)
#else
:cOsdMenu(Base ? Base : tr("Recordings"), 9, 7)
#endif /* LIEMIKUUTIO */
{
  base = Base ? strdup(Base) : NULL;
  level = Setup.RecordingDirs ? Level : -1;
  Recordings.StateChanged(recordingsState); // just to get the current state
  helpKeys = -1;
  Display(); // this keeps the higher level menus from showing up briefly when pressing 'Back' during replay
  Set();
  SetFreeDiskDisplay(true);
  if (Current() < 0)
     SetCurrent(First());
  else if (OpenSubMenus && cReplayControl::LastReplayed() && Open(true))
     return;
  Display();
  SetHelpKeys();
}

cMenuRecordings::~cMenuRecordings()
{
  helpKeys = -1;
  free(base);
}

bool cMenuRecordings::SetFreeDiskDisplay(bool Force)
{
  if (FreeDiskSpace.HasChanged(Force)) {
     //XXX -> skin function!!!
     SetTitle(cString::sprintf("%s  -  %s", base ? base : tr("Recordings"), FreeDiskSpace.FreeDiskSpaceString()));
     return true;
     }
  return false;
}

void cMenuRecordings::SetHelpKeys(void)
{
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  int NewHelpKeys = 0;
  if (ri) {
     if (ri->IsDirectory())
        NewHelpKeys = 1;
     else {
        NewHelpKeys = 2;
        cRecording *recording = GetRecording(ri);
        if (recording && recording->Info()->Title())
           NewHelpKeys = 3;
        }
     }
  if (NewHelpKeys != helpKeys) {
     switch (NewHelpKeys) {
       case 0: SetHelp(NULL); break;
       case 1: SetHelp(tr("Button$Open")); break;
       case 2:
       case 3: SetHelp(RecordingCommands.Count() ? tr("Commands") : tr("Button$Play"), tr("Button$Rewind"), tr("Button$Delete"), NewHelpKeys == 3 ? tr("Button$Info") : NULL);
       }
     helpKeys = NewHelpKeys;
     }
}

void cMenuRecordings::Set(bool Refresh)
{
  const char *CurrentRecording = cReplayControl::LastReplayed();
  cMenuRecordingItem *LastItem = NULL;
  char *LastItemText = NULL;
  cThreadLock RecordingsLock(&Recordings);
  if (Refresh) {
     cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
     if (ri) {
        cRecording *Recording = Recordings.GetByName(ri->FileName());
        if (Recording)
           CurrentRecording = Recording->FileName();
        }
     }
  Clear();
  Recordings.Sort();
  for (cRecording *recording = Recordings.First(); recording; recording = Recordings.Next(recording)) {
      if (!base || (strstr(recording->Name(), base) == recording->Name() && recording->Name()[strlen(base)] == '~')) {
         cMenuRecordingItem *Item = new cMenuRecordingItem(recording, level);
#ifdef USE_PINPLUGIN
         if ((*Item->Text() && (!LastItem || strcmp(Item->Text(), LastItemText) != 0))
            && (!cStatus::MsgReplayProtected(GetRecording(Item), Item->Name(), base, Item->IsDirectory(), true))) {
#else
         if (*Item->Text() && (!LastItem || strcmp(Item->Text(), LastItemText) != 0)) {
#endif /* PINPLUGIN */
            Add(Item);
            LastItem = Item;
            free(LastItemText);
            LastItemText = strdup(LastItem->Text()); // must use a copy because of the counters!
            }
         else
            delete Item;
         if (LastItem) {
            if (CurrentRecording && strcmp(CurrentRecording, recording->FileName()) == 0)
               SetCurrent(LastItem);
            if (LastItem->IsDirectory())
               LastItem->IncrementCounter(recording->IsNew());
            }
         }
      }
  free(LastItemText);
  Refresh |= SetFreeDiskDisplay(Refresh);
  if (Refresh)
     Display();
}

cRecording *cMenuRecordings::GetRecording(cMenuRecordingItem *Item)
{
  cRecording *recording = Recordings.GetByName(Item->FileName());
  if (!recording)
     Skins.Message(mtError, tr("Error while accessing recording!"));
  return recording;
}

bool cMenuRecordings::Open(bool OpenSubMenus)
{
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri && ri->IsDirectory()) {
     const char *t = ri->Name();
     cString buffer;
     if (base) {
        buffer = cString::sprintf("%s~%s", base, t);
        t = buffer;
        }
     AddSubMenu(new cMenuRecordings(t, level + 1, OpenSubMenus));
     return true;
     }
  return false;
}

eOSState cMenuRecordings::Play(void)
{
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri) {
#ifdef USE_PINPLUGIN
     if (cStatus::MsgReplayProtected(GetRecording(ri), ri->Name(), base, ri->IsDirectory()) == true)
        return osContinue;
#endif /* PINPLUGIN */
     if (ri->IsDirectory())
        Open();
     else {
        cRecording *recording = GetRecording(ri);
        if (recording) {
#ifdef USE_DVDARCHIVE
           int mountRet = MOUNT_DVD_REPLAY;
           if (recording->IsOnlyOnDvd()) {
              mountRet = recording->MountDvd();
              }
           if (mountRet == MOUNT_DVD_REPLAY) {
              cReplayControl::SetRecording(recording->FileName(), recording->Title());
              return osReplay;
              }
           else if (mountRet == MOUNT_DVD_LAUNCH_DVD_PLUGIN) {
              //launch DVD plugin here
              cPlugin *p = cPluginManager::GetPlugin("dvd");
              cOsdObject *osd = NULL;
              if (p) {
                 osd = p->MainMenuAction();
                 delete osd;
                 osd = NULL;
                 return osEnd;
                 }
              else {
                 Skins.Message(mtError, tr("DVD plugin is not installed!"));
                 Skins.Flush();
                 }
              }
#else
           cReplayControl::SetRecording(recording->FileName(), recording->Title());
           return osReplay;
#endif /* DVDARCHIVE */
           }
        }
     }
  return osContinue;
}

eOSState cMenuRecordings::Rewind(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri && !ri->IsDirectory()) {
     cDevice::PrimaryDevice()->StopReplay(); // must do this first to be able to rewind the currently replayed recording
     cResumeFile ResumeFile(ri->FileName());
     ResumeFile.Delete();
     return Play();
     }
  return osContinue;
}

eOSState cMenuRecordings::Delete(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri && !ri->IsDirectory()) {
     if (Interface->Confirm(tr("Delete recording?"))) {
        cRecordControl *rc = cRecordControls::GetRecordControl(ri->FileName());
        if (rc) {
           if (Interface->Confirm(tr("Timer still recording - really delete?"))) {
              cTimer *timer = rc->Timer();
              if (timer) {
                 timer->Skip();
                 cRecordControls::Process(time(NULL));
                 if (timer->IsSingleEvent()) {
                    isyslog("deleting timer %s", *timer->ToDescr());
                    Timers.Del(timer);
                    }
                 Timers.SetModified();
                 }
              }
           else
              return osContinue;
           }
        cRecording *recording = GetRecording(ri);
        if (recording) {
           if (cReplayControl::NowReplaying() && strcmp(cReplayControl::NowReplaying(), ri->FileName()) == 0)
              cControl::Shutdown();
           if (recording->Delete()) {
              cReplayControl::ClearLastReplayed(ri->FileName());
              Recordings.DelByName(ri->FileName());
              cOsdMenu::Del(Current());
              SetHelpKeys();
              SetFreeDiskDisplay(true);
              Display();
              if (!Count())
                 return osBack;
              }
           else
              Skins.Message(mtError, tr("Error while deleting recording!"));
           }
        }
     }
  return osContinue;
}

eOSState cMenuRecordings::Info(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri && !ri->IsDirectory()) {
     cRecording *recording = GetRecording(ri);
     if (recording && recording->Info()->Title())
        return AddSubMenu(new cMenuRecording(recording, true));
     }
  return osContinue;
}

eOSState cMenuRecordings::Commands(eKeys Key)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri && !ri->IsDirectory()) {
     cRecording *recording = GetRecording(ri);
     if (recording) {
        cMenuCommands *menu;
        eOSState state = AddSubMenu(menu = new cMenuCommands(tr("Recording commands"), &RecordingCommands, cString::sprintf("\"%s\"", *strescape(recording->FileName(), "\\\"$"))));
        if (Key != kNone)
           state = menu->ProcessKey(Key);
        return state;
        }
     }
  return osContinue;
}

#ifdef USE_LIEMIKUUTIO
eOSState cMenuRecordings::Rename(void)
{
  if (HasSubMenu() || Count() == 0)
     return osContinue;
  cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
  if (ri && !ri->IsDirectory()) {
     cRecording *recording = GetRecording(ri);
     if (recording)
        return AddSubMenu(new cMenuRenameRecording(recording));
     }
  return osContinue;
}
#endif /* LIEMIKUUTIO */

eOSState cMenuRecordings::ProcessKey(eKeys Key)
{
  bool HadSubMenu = HasSubMenu();
  eOSState state = cOsdMenu::ProcessKey(Key);

  if (state == osUnknown) {
#ifdef USE_SORTRECORDS
     const char *RecordingsSortModeTexts[MAXSORTMODES];
     RecordingsSortModeTexts[0] = tr("main dir alphabetically, subdirs flexible");
     RecordingsSortModeTexts[1] = tr("main dir by date, subdirs flexible");
     RecordingsSortModeTexts[2] = tr("all alphabetically");
     RecordingsSortModeTexts[3] = tr("all by date");
#endif /* SORTRECORDS */
     switch (Key) {
       case kPlay:
       case kOk:     return Play();
       case kRed:    return (helpKeys > 1 && RecordingCommands.Count()) ? Commands() : Play();
       case kGreen:  return Rewind();
       case kYellow: return Delete();
       case kInfo:
       case kBlue:   return Info();
#ifdef USE_SORTRECORDS
       case k0:      Setup.RecordingsSortMode = ++Setup.RecordingsSortMode % MAXSORTMODES;
                     Set(true);
                     Skins.Message(mtStatus, cString::sprintf("%s %d: %s", tr("Sorting"), Setup.RecordingsSortMode, RecordingsSortModeTexts[Setup.RecordingsSortMode]));
                     return osContinue;
       case k1...k7: return Commands(Key);
       case k8:      return Rename();
       case k9:      Recordings.ToggleSortOrder();
                     Set(true);
                     return osContinue;
#elif defined (USE_LIEMIKUUTIO)
       case k0:      DirOrderState = !DirOrderState;
                     Set(true);
                     return osContinue;
       case k8:      return Rename();
       case k9:
       case k1...k7: return Commands(Key);
#else
       case k1...k9: return Commands(Key);
#endif /* LIEMIKUUTIO & SORTRECORDS */
       case kNone:   if (Recordings.StateChanged(recordingsState))
                        Set(true);
                     break;
       default: break;
       }
     }
  if (Key == kYellow && HadSubMenu && !HasSubMenu()) {
     // the last recording in a subdirectory was deleted, so let's go back up
     cOsdMenu::Del(Current());
     if (!Count())
        return osBack;
     Display();
     }
  if (!HasSubMenu()) {
     if (HadSubMenu)
        SetFreeDiskDisplay();
     if (Key != kNone)
        SetHelpKeys();
     }
  return state;
}

// --- cMenuSetupBase --------------------------------------------------------

class cMenuSetupBase : public cMenuSetupPage {
protected:
  cSetup data;
  virtual void Store(void);
public:
  cMenuSetupBase(void);
  };

cMenuSetupBase::cMenuSetupBase(void)
{
  data = Setup;
}

void cMenuSetupBase::Store(void)
{
  Setup = data;
  Setup.Save();
}

// --- cMenuSetupOSD ---------------------------------------------------------

class cMenuSetupOSD : public cMenuSetupBase {
private:
  const char *useSmallFontTexts[3];
#ifdef USE_LIEMIKUUTIO
  const char *mainMenuTitle[MAXMAINMENUTITLE];
#endif /* LIEMIKUUTIO */
  int osdLanguageIndex;
  int numSkins;
  int originalSkinIndex;
  int skinIndex;
  const char **skinDescriptions;
  cThemes themes;
  int originalThemeIndex;
  int themeIndex;
  cStringList fontOsdNames, fontSmlNames, fontFixNames;
  int fontOsdIndex, fontSmlIndex, fontFixIndex;
  virtual void Set(void);
public:
  cMenuSetupOSD(void);
  virtual ~cMenuSetupOSD();
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetupOsd"; }
#endif /* GRAPHTFT */
  };

cMenuSetupOSD::cMenuSetupOSD(void)
{
  osdLanguageIndex = I18nCurrentLanguage();
  numSkins = Skins.Count();
  skinIndex = originalSkinIndex = Skins.Current()->Index();
  skinDescriptions = new const char*[numSkins];
  themes.Load(Skins.Current()->Name());
  themeIndex = originalThemeIndex = Skins.Current()->Theme() ? themes.GetThemeIndex(Skins.Current()->Theme()->Description()) : 0;
  cFont::GetAvailableFontNames(&fontOsdNames);
  cFont::GetAvailableFontNames(&fontSmlNames);
  cFont::GetAvailableFontNames(&fontFixNames, true);
  fontOsdNames.Insert(strdup(DefaultFontOsd));
  fontSmlNames.Insert(strdup(DefaultFontSml));
  fontFixNames.Insert(strdup(DefaultFontFix));
  fontOsdIndex = max(0, fontOsdNames.Find(Setup.FontOsd));
  fontSmlIndex = max(0, fontSmlNames.Find(Setup.FontSml));
  fontFixIndex = max(0, fontFixNames.Find(Setup.FontFix));
  Set();
}

cMenuSetupOSD::~cMenuSetupOSD()
{
  delete[] skinDescriptions;
}

void cMenuSetupOSD::Set(void)
{
  int current = Current();
  for (cSkin *Skin = Skins.First(); Skin; Skin = Skins.Next(Skin))
      skinDescriptions[Skin->Index()] = Skin->Description();
  useSmallFontTexts[0] = tr("never");
  useSmallFontTexts[1] = tr("skin dependent");
  useSmallFontTexts[2] = tr("always");
#ifdef USE_LIEMIKUUTIO
  mainMenuTitle[0]=tr("default");
  mainMenuTitle[1]=tr("VDR - text");
  mainMenuTitle[2]=tr("text");
  mainMenuTitle[3]=tr("VDR - version");
#endif /* LIEMIKUUTIO */
  Clear();
  SetSection(tr("OSD"));
  Add(new cMenuEditStraItem(tr("Setup.OSD$Language"),               &osdLanguageIndex, I18nNumLanguagesWithLocale(), &I18nLanguages()->At(0)));
  Add(new cMenuEditStraItem(tr("Setup.OSD$Skin"),                   &skinIndex, numSkins, skinDescriptions));
  if (themes.NumThemes())
  Add(new cMenuEditStraItem(tr("Setup.OSD$Theme"),                  &themeIndex, themes.NumThemes(), themes.Descriptions()));
#ifdef USE_WAREAGLEICON
  Add(new cMenuEditBoolItem(tr("Setup.OSD$WarEagle icons"),         &data.WarEagleIcons));
#endif /* WAREAGLEICON */
  Add(new cMenuEditIntItem( tr("Setup.OSD$Left"),                   &data.OSDLeft, 0, MAXOSDWIDTH));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Top"),                    &data.OSDTop, 0, MAXOSDHEIGHT));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Width"),                  &data.OSDWidth, MINOSDWIDTH, MAXOSDWIDTH));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Height"),                 &data.OSDHeight, MINOSDHEIGHT, MAXOSDHEIGHT));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Message time (s)"),       &data.OSDMessageTime, 1, 60));
  Add(new cMenuEditStraItem(tr("Setup.OSD$Use small font"),         &data.UseSmallFont, 3, useSmallFontTexts));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Anti-alias"),             &data.AntiAlias));
  Add(new cMenuEditStraItem(tr("Setup.OSD$Default font"),           &fontOsdIndex, fontOsdNames.Size(), &fontOsdNames[0]));
  Add(new cMenuEditStraItem(tr("Setup.OSD$Small font"),             &fontSmlIndex, fontSmlNames.Size(), &fontSmlNames[0]));
  Add(new cMenuEditStraItem(tr("Setup.OSD$Fixed font"),             &fontFixIndex, fontFixNames.Size(), &fontFixNames[0]));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Default font size (pixel)"), &data.FontOsdSize, 10, MAXFONTSIZE));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Small font size (pixel)"),&data.FontSmlSize, 10, MAXFONTSIZE));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Fixed font size (pixel)"),&data.FontFixSize, 10, MAXFONTSIZE));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Channel info position"),  &data.ChannelInfoPos, tr("bottom"), tr("top")));
  Add(new cMenuEditIntItem( tr("Setup.OSD$Channel info time (s)"),  &data.ChannelInfoTime, 1, 60));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Info on channel switch"), &data.ShowInfoOnChSwitch));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Timeout requested channel info"), &data.TimeoutRequChInfo));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Scroll pages"),           &data.MenuScrollPage));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Scroll wraps"),           &data.MenuScrollWrap));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Menu key closes"),        &data.MenuKeyCloses));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Recording directories"),  &data.RecordingDirs));
#ifdef USE_LIEMIKUUTIO
  Add(new cMenuEditStraItem(tr("Setup.OSD$Main menu title"),        &data.MainMenuTitle, MAXMAINMENUTITLE, mainMenuTitle));
  if (data.MainMenuTitle == 1 || data.MainMenuTitle == 2)
     Add(new cMenuEditStrItem(tr("Setup.OSD$- Text"),                data.CustomMainMenuTitle, sizeof(data.CustomMainMenuTitle)));
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Main menu command position"), &data.MenuCmdPosition, tr("bottom"), tr("top")));
#endif /* LIEMIKUUTIO */
#ifdef USE_VALIDINPUT
  Add(new cMenuEditBoolItem(tr("Setup.OSD$Show valid input"),       &data.ShowValidInput));
#endif /* VALIDINPUT */
  SetCurrent(Get(current));
  Display();
}

eOSState cMenuSetupOSD::ProcessKey(eKeys Key)
{
  bool ModifiedApperance = false;

  if (Key == kOk) {
     I18nSetLocale(data.OSDLanguage);
     if (skinIndex != originalSkinIndex) {
        cSkin *Skin = Skins.Get(skinIndex);
        if (Skin) {
           Utf8Strn0Cpy(data.OSDSkin, Skin->Name(), sizeof(data.OSDSkin));
           Skins.SetCurrent(Skin->Name());
           ModifiedApperance = true;
           }
        }
     if (themes.NumThemes() && Skins.Current()->Theme()) {
        Skins.Current()->Theme()->Load(themes.FileName(themeIndex));
        Utf8Strn0Cpy(data.OSDTheme, themes.Name(themeIndex), sizeof(data.OSDTheme));
        ModifiedApperance |= themeIndex != originalThemeIndex;
        }
     if (data.OSDLeft != Setup.OSDLeft || data.OSDTop != Setup.OSDTop || data.OSDWidth != Setup.OSDWidth || data.OSDHeight != Setup.OSDHeight) {
        data.OSDWidth &= ~0x07; // OSD width must be a multiple of 8
        ModifiedApperance = true;
        }
     if (data.UseSmallFont != Setup.UseSmallFont || data.AntiAlias != Setup.AntiAlias)
        ModifiedApperance = true;
     Utf8Strn0Cpy(data.FontOsd, fontOsdNames[fontOsdIndex], sizeof(data.FontOsd));
     Utf8Strn0Cpy(data.FontSml, fontSmlNames[fontSmlIndex], sizeof(data.FontSml));
     Utf8Strn0Cpy(data.FontFix, fontFixNames[fontFixIndex], sizeof(data.FontFix));
     if (strcmp(data.FontOsd, Setup.FontOsd) || data.FontOsdSize != Setup.FontOsdSize) {
        cFont::SetFont(fontOsd, data.FontOsd, data.FontOsdSize);
        ModifiedApperance = true;
        }
     if (strcmp(data.FontSml, Setup.FontSml) || data.FontSmlSize != Setup.FontSmlSize) {
        cFont::SetFont(fontSml, data.FontSml, data.FontSmlSize);
        ModifiedApperance = true;
        }
     if (strcmp(data.FontFix, Setup.FontFix) || data.FontFixSize != Setup.FontFixSize) {
        cFont::SetFont(fontFix, data.FontFix, data.FontFixSize);
        ModifiedApperance = true;
        }
     }

  int oldSkinIndex = skinIndex;
  int oldOsdLanguageIndex = osdLanguageIndex;
#ifdef USE_LIEMIKUUTIO
  int oldMainMenuTitle = data.MainMenuTitle;
#endif /* LIEMIKUUTIO */
  eOSState state = cMenuSetupBase::ProcessKey(Key);

  if (ModifiedApperance)
     SetDisplayMenu();

  if (osdLanguageIndex != oldOsdLanguageIndex || skinIndex != oldSkinIndex) {
     strn0cpy(data.OSDLanguage, I18nLocale(osdLanguageIndex), sizeof(data.OSDLanguage));
     int OriginalOSDLanguage = I18nCurrentLanguage();
     I18nSetLanguage(osdLanguageIndex);

     cSkin *Skin = Skins.Get(skinIndex);
     if (Skin) {
        char *d = themes.NumThemes() ? strdup(themes.Descriptions()[themeIndex]) : NULL;
        themes.Load(Skin->Name());
        if (skinIndex != oldSkinIndex)
           themeIndex = d ? themes.GetThemeIndex(d) : 0;
        free(d);
        }

     Set();
     I18nSetLanguage(OriginalOSDLanguage);
     }

#ifdef USE_LIEMIKUUTIO
  if (data.MainMenuTitle != oldMainMenuTitle)
     Set();
#endif /* LIEMIKUUTIO */
  return state;
}

// --- cMenuSetupEPG ---------------------------------------------------------

class cMenuSetupEPG : public cMenuSetupBase {
private:
#ifdef USE_NOEPG
  const char *noEPGModes[2];
#endif /* NOEPG */
  int originalNumLanguages;
  int numLanguages;
  void Setup(void);
public:
  cMenuSetupEPG(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetupEpg"; }
#endif /* GRAPHTFT */
  };

cMenuSetupEPG::cMenuSetupEPG(void)
{
  for (numLanguages = 0; numLanguages < I18nLanguages()->Size() && data.EPGLanguages[numLanguages] >= 0; numLanguages++)
      ;
  originalNumLanguages = numLanguages;
  SetSection(tr("EPG"));
  SetHelp(tr("Button$Scan"));
  Setup();
}

void cMenuSetupEPG::Setup(void)
{
  int current = Current();

#ifdef USE_NOEPG
  noEPGModes[0]=tr("Blacklist");
  noEPGModes[1]=tr("Whitelist");
#endif /* NOEPG */

  Clear();

  Add(new cMenuEditIntItem( tr("Setup.EPG$EPG scan timeout (h)"),      &data.EPGScanTimeout));
  Add(new cMenuEditIntItem( tr("Setup.EPG$EPG bugfix level"),          &data.EPGBugfixLevel, 0, MAXEPGBUGFIXLEVEL));
  Add(new cMenuEditIntItem( tr("Setup.EPG$EPG linger time (min)"),     &data.EPGLinger, 0));
#ifdef USE_LIEMIKUUTIO
  Add(new cMenuEditBoolItem(tr("Setup.EPG$Show progress bar"),         &data.ShowProgressBar));
#endif /* LIEMIKUUTIO */
  Add(new cMenuEditBoolItem(tr("Setup.EPG$Set system time"),           &data.SetSystemTime));
  if (data.SetSystemTime)
     Add(new cMenuEditTranItem(tr("Setup.EPG$Use time from transponder"), &data.TimeTransponder, &data.TimeSource));
  // TRANSLATORS: note the plural!
  Add(new cMenuEditIntItem( tr("Setup.EPG$Preferred languages"),       &numLanguages, 0, I18nLanguages()->Size()));
  for (int i = 0; i < numLanguages; i++)
      // TRANSLATORS: note the singular!
      Add(new cMenuEditStraItem(tr("Setup.EPG$Preferred language"),    &data.EPGLanguages[i], I18nLanguages()->Size(), &I18nLanguages()->At(0)));
#ifdef USE_DDEPGENTRY
  Add(new cMenuEditIntItem( tr("Setup.EPG$Period for double EPG search(min)"),  &data.DoubleEpgTimeDelta));
  Add(new cMenuEditBoolItem(tr("Setup.EPG$Extern double Epg entry"),   &data.DoubleEpgAction, tr("adjust"), tr("delete")));
  Add(new cMenuEditBoolItem(tr("Setup.EPG$Mix intern and extern EPG"), &data.MixEpgAction));
  Add(new cMenuEditBoolItem(tr("Setup.EPG$Disable running VPS event"), &data.DisableVPS));
#endif /* DDEPGENTRY */
#ifdef USE_NOEPG
  Add(new cMenuEditStraItem(tr("Setup.EPG$Mode of noEPG-Patch"),       &data.noEPGMode, 2, noEPGModes));
#endif /* NOEPG */

  SetCurrent(Get(current));
  Display();
}

eOSState cMenuSetupEPG::ProcessKey(eKeys Key)
{
  if (Key == kOk) {
     bool Modified = numLanguages != originalNumLanguages;
     if (!Modified) {
        for (int i = 0; i < numLanguages; i++) {
            if (data.EPGLanguages[i] != ::Setup.EPGLanguages[i]) {
               Modified = true;
               break;
               }
            }
        }
     if (Modified)
        cSchedules::ResetVersions();
     }

  int oldnumLanguages = numLanguages;
  int oldSetSystemTime = data.SetSystemTime;

  eOSState state = cMenuSetupBase::ProcessKey(Key);
  if (Key != kNone) {
     if (numLanguages != oldnumLanguages || data.SetSystemTime != oldSetSystemTime) {
        for (int i = oldnumLanguages; i < numLanguages; i++) {
            data.EPGLanguages[i] = 0;
            for (int l = 0; l < I18nLanguages()->Size(); l++) {
                int k;
                for (k = 0; k < oldnumLanguages; k++) {
                    if (data.EPGLanguages[k] == l)
                       break;
                    }
                if (k >= oldnumLanguages) {
                   data.EPGLanguages[i] = l;
                   break;
                   }
                }
            }
        data.EPGLanguages[numLanguages] = -1;
        Setup();
        }
     if (Key == kRed) {
        EITScanner.ForceScan();
        return osEnd;
        }
     }
  return state;
}

// --- cMenuSetupDVB ---------------------------------------------------------

class cMenuSetupDVB : public cMenuSetupBase {
private:
  int originalNumAudioLanguages;
  int numAudioLanguages;
  int originalNumSubtitleLanguages;
  int numSubtitleLanguages;
  void Setup(void);
  const char *videoDisplayFormatTexts[3];
  const char *updateChannelsTexts[6];
public:
  cMenuSetupDVB(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetupDvb"; }
#endif /* GRAPHTFT */
  };

cMenuSetupDVB::cMenuSetupDVB(void)
{
  for (numAudioLanguages = 0; numAudioLanguages < I18nLanguages()->Size() && data.AudioLanguages[numAudioLanguages] >= 0; numAudioLanguages++)
      ;
  for (numSubtitleLanguages = 0; numSubtitleLanguages < I18nLanguages()->Size() && data.SubtitleLanguages[numSubtitleLanguages] >= 0; numSubtitleLanguages++)
      ;
  originalNumAudioLanguages = numAudioLanguages;
  originalNumSubtitleLanguages = numSubtitleLanguages;
  videoDisplayFormatTexts[0] = tr("pan&scan");
  videoDisplayFormatTexts[1] = tr("letterbox");
  videoDisplayFormatTexts[2] = tr("center cut out");
  updateChannelsTexts[0] = tr("no");
  updateChannelsTexts[1] = tr("names only");
  updateChannelsTexts[2] = tr("PIDs only");
  updateChannelsTexts[3] = tr("names and PIDs");
  updateChannelsTexts[4] = tr("add new channels");
  updateChannelsTexts[5] = tr("add new transponders");

  SetSection(tr("DVB"));
  Setup();
}

void cMenuSetupDVB::Setup(void)
{
  int current = Current();

  Clear();

  Add(new cMenuEditIntItem( tr("Setup.DVB$Primary DVB interface"), &data.PrimaryDVB, 1, cDevice::NumDevices()));
  Add(new cMenuEditBoolItem(tr("Setup.DVB$Video format"),          &data.VideoFormat, "4:3", "16:9"));
  if (data.VideoFormat == 0)
     Add(new cMenuEditStraItem(tr("Setup.DVB$Video display format"), &data.VideoDisplayFormat, 3, videoDisplayFormatTexts));
  Add(new cMenuEditBoolItem(tr("Setup.DVB$Use Dolby Digital"),     &data.UseDolbyDigital));
  Add(new cMenuEditStraItem(tr("Setup.DVB$Update channels"),       &data.UpdateChannels, 6, updateChannelsTexts));
  Add(new cMenuEditIntItem( tr("Setup.DVB$Audio languages"),       &numAudioLanguages, 0, I18nLanguages()->Size()));
  for (int i = 0; i < numAudioLanguages; i++)
      Add(new cMenuEditStraItem(tr("Setup.DVB$Audio language"),    &data.AudioLanguages[i], I18nLanguages()->Size(), &I18nLanguages()->At(0)));
  Add(new cMenuEditBoolItem(tr("Setup.DVB$Display subtitles"),     &data.DisplaySubtitles));
  if (data.DisplaySubtitles) {
     Add(new cMenuEditIntItem( tr("Setup.DVB$Subtitle languages"),    &numSubtitleLanguages, 0, I18nLanguages()->Size()));
     for (int i = 0; i < numSubtitleLanguages; i++)
         Add(new cMenuEditStraItem(tr("Setup.DVB$Subtitle language"), &data.SubtitleLanguages[i], I18nLanguages()->Size(), &I18nLanguages()->At(0)));
     Add(new cMenuEditIntItem( tr("Setup.DVB$Subtitle offset"),                  &data.SubtitleOffset,      -100, 100));
     Add(new cMenuEditIntItem( tr("Setup.DVB$Subtitle foreground transparency"), &data.SubtitleFgTransparency, 0, 9));
     Add(new cMenuEditIntItem( tr("Setup.DVB$Subtitle background transparency"), &data.SubtitleBgTransparency, 0, 10));
     }
#ifdef USE_DVBSETUP
  Add(new cMenuEditBoolItem(tr("Setup.DVB$Use AC3-Transfer Fix"),          &data.DolbyTransferFix));
  Add(new cMenuEditBoolItem(tr("Setup.DVB$No Qam256 on Cards with Decoder"),   &data.NoQamOnDecoder));
  Add(new cMenuEditBoolItem(tr("Setup.DVB$Turn off Primary Tuner"),        &data.TurnOffPrimary));
#endif /* DVBSETUP */
#ifdef USE_SYNCEARLY
  Add(new cMenuEditBoolItem(tr("Setup.DVB$Use Sync Early Patch"),  &data.UseSyncEarlyPatch));
#endif /* SYNCEARLY */

  SetCurrent(Get(current));
  Display();
}

eOSState cMenuSetupDVB::ProcessKey(eKeys Key)
{
  int oldPrimaryDVB = ::Setup.PrimaryDVB;
  int oldVideoDisplayFormat = ::Setup.VideoDisplayFormat;
  bool oldVideoFormat = ::Setup.VideoFormat;
  bool newVideoFormat = data.VideoFormat;
  bool oldDisplaySubtitles = ::Setup.DisplaySubtitles;
  bool newDisplaySubtitles = data.DisplaySubtitles;
  int oldnumAudioLanguages = numAudioLanguages;
  int oldnumSubtitleLanguages = numSubtitleLanguages;
  eOSState state = cMenuSetupBase::ProcessKey(Key);

  if (Key != kNone) {
     bool DoSetup = data.VideoFormat != newVideoFormat;
     DoSetup |= data.DisplaySubtitles != newDisplaySubtitles;
     if (numAudioLanguages != oldnumAudioLanguages) {
        for (int i = oldnumAudioLanguages; i < numAudioLanguages; i++) {
            data.AudioLanguages[i] = 0;
            for (int l = 0; l < I18nLanguages()->Size(); l++) {
                int k;
                for (k = 0; k < oldnumAudioLanguages; k++) {
                    if (data.AudioLanguages[k] == l)
                       break;
                    }
                if (k >= oldnumAudioLanguages) {
                   data.AudioLanguages[i] = l;
                   break;
                   }
                }
            }
        data.AudioLanguages[numAudioLanguages] = -1;
        DoSetup = true;
        }
     if (numSubtitleLanguages != oldnumSubtitleLanguages) {
        for (int i = oldnumSubtitleLanguages; i < numSubtitleLanguages; i++) {
            data.SubtitleLanguages[i] = 0;
            for (int l = 0; l < I18nLanguages()->Size(); l++) {
                int k;
                for (k = 0; k < oldnumSubtitleLanguages; k++) {
                    if (data.SubtitleLanguages[k] == l)
                       break;
                    }
                if (k >= oldnumSubtitleLanguages) {
                   data.SubtitleLanguages[i] = l;
                   break;
                   }
                }
            }
        data.SubtitleLanguages[numSubtitleLanguages] = -1;
        DoSetup = true;
        }
     if (DoSetup)
        Setup();
     }
  if (state == osBack && Key == kOk) {
     if (::Setup.PrimaryDVB != oldPrimaryDVB)
        state = osSwitchDvb;
     if (::Setup.VideoDisplayFormat != oldVideoDisplayFormat)
        cDevice::PrimaryDevice()->SetVideoDisplayFormat(eVideoDisplayFormat(::Setup.VideoDisplayFormat));
     if (::Setup.VideoFormat != oldVideoFormat)
        cDevice::PrimaryDevice()->SetVideoFormat(::Setup.VideoFormat);
     if (::Setup.DisplaySubtitles != oldDisplaySubtitles)
        cDevice::PrimaryDevice()->EnsureSubtitleTrack();
     cDvbSubtitleConverter::SetupChanged();
     }
  return state;
}

// --- cMenuSetupLNB ---------------------------------------------------------

class cMenuSetupLNB : public cMenuSetupBase {
private:
  void Setup(void);
public:
  cMenuSetupLNB(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetupLnb"; }
#endif /* GRAPHTFT */
  };

cMenuSetupLNB::cMenuSetupLNB(void)
{
  SetSection(tr("LNB"));
  Setup();
}

void cMenuSetupLNB::Setup(void)
{
  int current = Current();

  Clear();

#ifdef USE_LNBSHARE
  int numSatDevices = 0;
  for (int i = 0; i < cDevice::NumDevices(); i++) {
      if (cDevice::GetDevice(i)->ProvidesSource(cSource::stSat)) numSatDevices++;
      }
  if (numSatDevices > 1) {
     char tmp[30];
     for (int i = 1; i <= cDevice::NumDevices(); i++) {
         if (cDevice::GetDevice(i - 1)->ProvidesSource(cSource::stSat)) {
            sprintf( tmp, tr("Setup.LNB$DVB device %d uses LNB No."), i);
            Add(new cMenuEditIntItem( tmp, &data.CardUsesLNBnr[i - 1], 1, numSatDevices ));
            }
         }
     }
  Add(new cMenuEditBoolItem(tr("Setup.LNB$Log LNB usage"), &data.VerboseLNBlog));
#endif /* LNBSHARE */

  Add(new cMenuEditBoolItem(tr("Setup.LNB$Use DiSEqC"),               &data.DiSEqC));
  if (!data.DiSEqC) {
     Add(new cMenuEditIntItem( tr("Setup.LNB$SLOF (MHz)"),               &data.LnbSLOF));
     Add(new cMenuEditIntItem( tr("Setup.LNB$Low LNB frequency (MHz)"),  &data.LnbFrequLo));
     Add(new cMenuEditIntItem( tr("Setup.LNB$High LNB frequency (MHz)"), &data.LnbFrequHi));
     }

  SetCurrent(Get(current));
  Display();
}

eOSState cMenuSetupLNB::ProcessKey(eKeys Key)
{
  int oldDiSEqC = data.DiSEqC;
  eOSState state = cMenuSetupBase::ProcessKey(Key);

#ifdef USE_LNBSHARE
  if (Key == kOk) cDevice::SetLNBnr();
#endif /* LNBSHARE */

  if (Key != kNone && data.DiSEqC != oldDiSEqC)
     Setup();
  return state;
}

// --- cMenuSetupCAM ---------------------------------------------------------

class cMenuSetupCAMItem : public cOsdItem {
private:
  cCamSlot *camSlot;
public:
  cMenuSetupCAMItem(cCamSlot *CamSlot);
  cCamSlot *CamSlot(void) { return camSlot; }
  bool Changed(void);
  };

cMenuSetupCAMItem::cMenuSetupCAMItem(cCamSlot *CamSlot)
{
  camSlot = CamSlot;
  SetText("");
  Changed();
}

bool cMenuSetupCAMItem::Changed(void)
{
  char buffer[32];
  const char *CamName = camSlot->GetCamName();
  if (!CamName) {
     switch (camSlot->ModuleStatus()) {
       case msReset:   CamName = tr("CAM reset"); break;
       case msPresent: CamName = tr("CAM present"); break;
       case msReady:   CamName = tr("CAM ready"); break;
       default:        CamName = "-"; break;
       }
     }
  snprintf(buffer, sizeof(buffer), " %d %s", camSlot->SlotNumber(), CamName);
  if (strcmp(buffer, Text()) != 0) {
     SetText(buffer);
     return true;
     }
  return false;
}

class cMenuSetupCAM : public cMenuSetupBase {
private:
  eOSState Menu(void);
  eOSState Reset(void);
public:
  cMenuSetupCAM(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetupCam"; }
#endif /* GRAPHTFT */
  };

cMenuSetupCAM::cMenuSetupCAM(void)
{
  SetSection(tr("CAM"));
  SetCols(15);
  SetHasHotkeys();
  for (cCamSlot *CamSlot = CamSlots.First(); CamSlot; CamSlot = CamSlots.Next(CamSlot))
      Add(new cMenuSetupCAMItem(CamSlot));
  SetHelp(tr("Button$Menu"), tr("Button$Reset"));
}

eOSState cMenuSetupCAM::Menu(void)
{
  cMenuSetupCAMItem *item = (cMenuSetupCAMItem *)Get(Current());
  if (item) {
     if (item->CamSlot()->EnterMenu()) {
        Skins.Message(mtStatus, tr("Opening CAM menu..."));
        time_t t0 = time(NULL);
        time_t t1 = t0;
        while (time(NULL) - t0 <= MAXWAITFORCAMMENU) {
              if (item->CamSlot()->HasUserIO())
                 break;
              if (time(NULL) - t1 >= CAMMENURETYTIMEOUT) {
                 dsyslog("CAM %d: retrying to enter CAM menu...", item->CamSlot()->SlotNumber());
                 item->CamSlot()->EnterMenu();
                 t1 = time(NULL);
                 }
              cCondWait::SleepMs(100);
              }
        Skins.Message(mtStatus, NULL);
        if (item->CamSlot()->HasUserIO())
           return AddSubMenu(new cMenuCam(item->CamSlot()));
        }
     Skins.Message(mtError, tr("Can't open CAM menu!"));
     }
  return osContinue;
}

eOSState cMenuSetupCAM::Reset(void)
{
  cMenuSetupCAMItem *item = (cMenuSetupCAMItem *)Get(Current());
  if (item) {
     if (!item->CamSlot()->Device() || Interface->Confirm(tr("CAM is in use - really reset?"))) {
        if (!item->CamSlot()->Reset())
           Skins.Message(mtError, tr("Can't reset CAM!"));
        }
     }
  return osContinue;
}

eOSState cMenuSetupCAM::ProcessKey(eKeys Key)
{
  eOSState state = HasSubMenu() ? cMenuSetupBase::ProcessKey(Key) : cOsdMenu::ProcessKey(Key);

  if (!HasSubMenu()) {
     switch (Key) {
       case kOk:
       case kRed:    return Menu();
       case kGreen:  state = Reset(); break;
       default: break;
       }
     for (cMenuSetupCAMItem *ci = (cMenuSetupCAMItem *)First(); ci; ci = (cMenuSetupCAMItem *)ci->Next()) {
         if (ci->Changed())
            DisplayItem(ci);
         }
     }
  return state;
}

// --- cMenuSetupRecord ------------------------------------------------------

class cMenuSetupRecord : public cMenuSetupBase {
#ifdef USE_SORTRECORDS
private:
  const char *RecordingsSortModeTexts[MAXSORTMODES];
#endif /* SORTRECORDS */
#ifdef USE_DELTIMESHIFTREC
private:
  const char *DelTimeshiftRecValues[3];
#endif /* DELTIMESHIFTREC */
public:
  cMenuSetupRecord(void);
#ifdef USE_DVLVIDPREFER
  eOSState ProcessKey(eKeys key);

private:
  void Set(void);

  int tmpNVidPrefer,
      tmpUseVidPrefer;
#endif /* DVLVIDPREFER */
  };

#ifdef USE_DVLVIDPREFER
cMenuSetupRecord::cMenuSetupRecord(void)
{
  Set();
}

eOSState cMenuSetupRecord::ProcessKey(eKeys key)
{
  eOSState s = cMenuSetupBase::ProcessKey(key);;

  if (key != kNone) {
     if (tmpNVidPrefer != data.nVidPrefer || tmpUseVidPrefer != data.UseVidPrefer) {
        int cur = Current();

        tmpNVidPrefer = data.nVidPrefer;
        tmpUseVidPrefer = data.UseVidPrefer;

        Clear();
        Set();
        SetCurrent(Get(cur));
        Display();
        cMenuSetupBase::ProcessKey(kNone);
        return osContinue;
        }
     }
  return s;
}

#else
cMenuSetupRecord::cMenuSetupRecord(void)
#endif /* DVLVIDPREFER */
#ifdef USE_DVLVIDPREFER
void cMenuSetupRecord::Set(void)
#endif /* DVLVIDPREFER */
{
#ifdef USE_SORTRECORDS
  RecordingsSortModeTexts[0] = tr("main dir alphabetically, subdirs flexible");
  RecordingsSortModeTexts[1] = tr("main dir by date, subdirs flexible");
  RecordingsSortModeTexts[2] = tr("all alphabetically");
  RecordingsSortModeTexts[3] = tr("all by date");
#endif /* SORTRECORDS */
#ifdef USE_DELTIMESHIFTREC
  DelTimeshiftRecValues[0] = tr("request");
  DelTimeshiftRecValues[1] = tr("no");
  DelTimeshiftRecValues[2] = tr("yes");
#endif /* DELTIMESHIFTREC */
  SetSection(tr("Recording"));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Margin at start (min)"),     &data.MarginStart));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Margin at stop (min)"),      &data.MarginStop));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Primary limit"),             &data.PrimaryLimit, 0, MAXPRIORITY));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Default priority"),          &data.DefaultPriority, 0, MAXPRIORITY));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Default lifetime (d)"),      &data.DefaultLifetime, 0, MAXLIFETIME));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Pause priority"),            &data.PausePriority, 0, MAXPRIORITY));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Pause lifetime (d)"),        &data.PauseLifetime, 0, MAXLIFETIME));
#ifdef USE_DOLBYINREC
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Record Dolby Digital"),      &data.UseDolbyInRecordings));
#endif /* DOLBYINREC */
#ifdef USE_DVLVIDPREFER
  tmpNVidPrefer = data.nVidPrefer;
  tmpUseVidPrefer = data.UseVidPrefer;

  Add(new cMenuEditBoolItem(tr("Setup.Recording$Video directory policy"),    &data.UseVidPrefer));
  if (data.UseVidPrefer != 0) {
     char tmp[ 64 ];
     Add(new cMenuEditIntItem(tr("Setup.Recording$Number of video directories"), &data.nVidPrefer, 1, DVLVIDPREFER_MAX));
     for (int zz = 0; zz < data.nVidPrefer; zz++) {
         sprintf(tmp, tr("Setup.Recording$Video %d priority"), zz);
         Add(new cMenuEditIntItem(tmp, &data.VidPreferPrio[ zz ], 0, 99));
         sprintf(tmp, tr("Setup.Recording$Video %d min. free MB"), zz);
         Add(new cMenuEditIntItem(tmp, &data.VidPreferSize[ zz ], -1, 99999));
         }
     }
#endif /* DVLVIDPREFER */
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Use episode name"),          &data.UseSubtitle));
#ifdef USE_DVLFRIENDLYFNAMES
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Friendly filenames"),        &data.UseFriendlyFNames));
#endif /* DVLFRIENDLYFNAMES */
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Use VPS"),                   &data.UseVps));
  Add(new cMenuEditIntItem( tr("Setup.Recording$VPS margin (s)"),            &data.VpsMargin, 0));
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Mark instant recording"),    &data.MarkInstantRecord));
  Add(new cMenuEditStrItem( tr("Setup.Recording$Name instant recording"),     data.NameInstantRecord, sizeof(data.NameInstantRecord)));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Instant rec. time (min)"),   &data.InstantRecordTime, 1, MAXINSTANTRECTIME));
  Add(new cMenuEditIntItem( tr("Setup.Recording$Max. video file size (MB)"), &data.MaxVideoFileSize, MINVIDEOFILESIZE, MAXVIDEOFILESIZE));
#ifdef USE_HARDLINKCUTTER
  Add(new cMenuEditIntItem( tr("Setup.Recording$Max. recording size (GB)"),  &data.MaxRecordingSize, MINRECORDINGSIZE, MAXRECORDINGSIZE));
#endif /* HARDLINKCUTTER */
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Split edited files"),        &data.SplitEditedFiles));
#ifdef USE_HARDLINKCUTTER
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Hard Link Cutter"),          &data.HardLinkCutter));
#endif /* HARDLINKCUTTER */
#ifdef USE_DELTIMESHIFTREC
  Add(new cMenuEditStraItem(tr("Setup.Recording$Delete timeshift recording"), &data.DelTimeshiftRec, 3, DelTimeshiftRecValues));
#endif /* DELTIMESHIFTREC */
#ifdef USE_LIEMIKUUTIO
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Show date"),                 &data.ShowRecDate));
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Show time"),                 &data.ShowRecTime));
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Show length"),               &data.ShowRecLength));
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Show end of timer"),         &data.ShowTimerStop));
#endif /* LIEMIKUUTIO */
#ifdef USE_SORTRECORDS
  Add(new cMenuEditStraItem(tr("Setup.Recording$Sort recordings by"),        &data.RecordingsSortMode, MAXSORTMODES, RecordingsSortModeTexts));
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Sort directories before recordings"), &data.RecordingsSortDirsFirst));
#endif /* SORTRECORDS */
#ifdef USE_CUTTERQUEUE
  Add(new cMenuEditBoolItem(tr("Setup.Recording$Cutter auto delete"),        &data.CutterAutoDelete));
#endif /* CUTTERQUEUE */
}

// --- cMenuSetupReplay ------------------------------------------------------

class cMenuSetupReplay : public cMenuSetupBase {
protected:
  virtual void Store(void);
public:
  cMenuSetupReplay(void);
  };

cMenuSetupReplay::cMenuSetupReplay(void)
{
  SetSection(tr("Replay"));
  Add(new cMenuEditBoolItem(tr("Setup.Replay$Multi speed mode"), &data.MultiSpeedMode));
  Add(new cMenuEditBoolItem(tr("Setup.Replay$Show replay mode"), &data.ShowReplayMode));
  Add(new cMenuEditIntItem(tr("Setup.Replay$Resume ID"), &data.ResumeID, 0, 99));
#ifdef USE_JUMPPLAY
  Add(new cMenuEditBoolItem(tr("Setup.Replay$Jump&Play"),        &data.JumpPlay));
  Add(new cMenuEditBoolItem(tr("Setup.Replay$Play&Jump"),        &data.PlayJump));
  Add(new cMenuEditBoolItem(tr("Setup.Replay$Pause at last mark"), &data.PauseLastMark));
  Add(new cMenuEditBoolItem(tr("Setup.Replay$Reload marks"),     &data.ReloadMarks));
#endif /* JUMPPLAY */
#ifdef USE_LIEMIKUUTIO
  Add(new cMenuEditIntItem(tr("Setup.Replay$Skip Seconds"),      &data.JumpSeconds));
  Add(new cMenuEditIntItem(tr("Setup.Replay$Skip Seconds Slow"), &data.JumpSecondsSlow));
#endif /* LIEMIKUUTIO */
#ifdef USE_DVDARCHIVE
  static const char *dvddisplaymode[3];
  dvddisplaymode[0]=tr("Setup.Replay$Length");
  dvddisplaymode[1]=tr("Setup.Replay$Length / Number");
  dvddisplaymode[2]=tr("Setup.Replay$Number");
  Add(new cMenuEditStraItem(tr("Setup.Replay$DVD display mode"),                &data.DvdDisplayMode,3,dvddisplaymode));
  Add(new cMenuEditBoolItem(tr("Setup.Replay$DVD display leading zeros"),       &data.DvdDisplayZeros));
  static const char *dvdtraymode[4];
  dvdtraymode[0]=tr("Setup.Replay$never");
  dvdtraymode[1]=tr("Setup.Replay$on begin");
  dvdtraymode[2]=tr("Setup.Replay$on end");
  dvdtraymode[3]=tr("Setup.Replay$on begin and end");
  Add(new cMenuEditStraItem(tr("Setup.Replay$Tray open"),                       &data.DvdTrayMode,4,dvdtraymode));
  Add(new cMenuEditIntItem( tr("Setup.Replay$Limit DVD to speed"),              &data.DvdSpeedLimit, 0, 50));
#endif /* DVDARCHIVE */
}

void cMenuSetupReplay::Store(void)
{
  if (Setup.ResumeID != data.ResumeID)
     Recordings.ResetResume();
  cMenuSetupBase::Store();
}

// --- cMenuSetupMisc --------------------------------------------------------

class cMenuSetupMisc : public cMenuSetupBase {
#ifdef USE_VOLCTRL
private:
  const char *lrChannelGroupsTexts[3];
  const char *lrForwardRewindTexts[3];
  void Setup(void);
#endif /* VOLCTRL */
public:
  cMenuSetupMisc(void);
#ifdef USE_VOLCTRL
  virtual eOSState ProcessKey(eKeys Key);
#endif /* VOLCTRL */
  };

cMenuSetupMisc::cMenuSetupMisc(void)
{
#ifdef USE_VOLCTRL
  lrChannelGroupsTexts[0] = tr("no");
  lrChannelGroupsTexts[1] = tr("Setup.Miscellaneous$only in channelinfo");
  lrChannelGroupsTexts[2] = tr("yes");
  lrForwardRewindTexts[0] = tr("no");
  lrForwardRewindTexts[1] = tr("Setup.Miscellaneous$only in progress display");
  lrForwardRewindTexts[2] = tr("yes");
#endif /* VOLCTRL */
  SetSection(tr("Miscellaneous"));
#ifdef USE_VOLCTRL
  Setup();
}

eOSState cMenuSetupMisc::ProcessKey(eKeys Key)
{
  int newLRVolumeControl = data.LRVolumeControl;
  eOSState state = cMenuSetupBase::ProcessKey(Key);
  if (Key != kNone && data.LRVolumeControl != newLRVolumeControl)
     Setup();
  return state;
}

void cMenuSetupMisc::Setup(void)
{
  int current = Current();
  Clear();
#endif /* VOLCTRL */
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Min. event timeout (min)"),   &data.MinEventTimeout));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Min. user inactivity (min)"), &data.MinUserInactivity));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$SVDRP timeout (s)"),          &data.SVDRPTimeout));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Zap timeout (s)"),            &data.ZapTimeout));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Channel entry timeout (ms)"), &data.ChannelEntryTimeout, 0));
  Add(new cMenuEditChanItem(tr("Setup.Miscellaneous$Initial channel"),            &data.InitialChannel, tr("Setup.Miscellaneous$as before")));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Initial volume"),             &data.InitialVolume, -1, 255, tr("Setup.Miscellaneous$as before")));
#ifdef USE_VOLCTRL
  Add(new cMenuEditBoolItem(tr("Setup.Miscellaneous$Volume ctrl with left/right"),     &data.LRVolumeControl));
  if (data.LRVolumeControl) {
     Add(new cMenuEditStraItem(tr("Setup.Miscellaneous$Channelgroups with left/right"),   &data.LRChannelGroups, 3, lrChannelGroupsTexts));
     Add(new cMenuEditStraItem(tr("Setup.Miscellaneous$Search fwd/back with left/right"), &data.LRForwardRewind, 3, lrForwardRewindTexts));
     }
  SetCurrent(Get(current));
  Display();
#endif /* VOLCTRL */
  Add(new cMenuEditBoolItem(tr("Setup.Miscellaneous$Emergency exit"),             &data.EmergencyExit));
#ifdef USE_LIRCSETTINGS
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Lirc repeat delay"),          &data.LircRepeatDelay, 0, 1000));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Lirc repeat freq"),           &data.LircRepeatFreq, 0, 1000));
  Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Lirc repeat timeout"),        &data.LircRepeatTimeout, 0, 5000));
#endif /* LIRCSETTINGS */
}

#ifdef USE_LIVEBUFFER
// --- cMenuSetupLiveBuffer --------------------------------------------------

class cMenuSetupLiveBuffer : public cMenuSetupBase {
private:
  void Setup();
public:
  eOSState ProcessKey(eKeys Key);
  cMenuSetupLiveBuffer(void);
  };

cMenuSetupLiveBuffer::cMenuSetupLiveBuffer(void)
{
  SetSection("permanentes Timeshift");
  Setup();
}

void cMenuSetupLiveBuffer::Setup(void)
{
  int current=Current();
  Clear();
  Add(new cMenuEditBoolItem(tr("Permanent Timeshift"),           &data.LiveBuffer));
  if (data.LiveBuffer) {
     Add(new cMenuEditBoolItem(tr("Setup.LiveBuffer$Location"),  &data.InRAM,tr("Setup.LiveBuffer$harddisk"),tr("Setup.LiveBuffer$memory")));
     if (data.InRAM) {
        Add(new cMenuEditIntItem( tr("Setup.LiveBuffer$Buffer size in memory (MB)"),       &data.MemBufSize, 5, 1000));
        Add(new cMenuEditBoolItem(tr("Setup.LiveBuffer$Extend to harddisk if necessary"),  &data.ExtendBuffer));
        Add(new cMenuEditBoolItem(tr("Setup.LiveBuffer$Keep paused Buffer"),               &data.KeepPaused));
        if (data.ExtendBuffer || data.KeepPaused)
           Add(new cMenuEditIntItem(tr("Setup.LiveBuffer$Buffer size on harddisk (MB)"),   &data.LiveBufferSize, 1, 100000));
        }
     else {
        Add(new cMenuEditIntItem( tr("Setup.LiveBuffer$Buffer size (MB)"),                 &data.LiveBufferSize, 1, 100000));
        Add(new cMenuEditBoolItem(tr("Setup.LiveBuffer$Keep paused Buffer"),               &data.KeepPaused));
        }
     }
  SetCurrent(Get(current));
  Display();
}

eOSState cMenuSetupLiveBuffer::ProcessKey(eKeys Key)
{
  int oldLiveBuffer = data.LiveBuffer;
  int oldInRAM = data.InRAM;
  int oldExtendBuffer = data.ExtendBuffer;
  int oldKeepPaused = data.KeepPaused;
  eOSState state = cMenuSetupBase::ProcessKey(Key);

  if (Key != kNone && (data.LiveBuffer != oldLiveBuffer || data.InRAM != oldInRAM || data.ExtendBuffer != oldExtendBuffer || data.KeepPaused != oldKeepPaused))
     Setup();
  return state;
}

#endif /* LIVEBUFFER */
// --- cMenuSetupPluginItem --------------------------------------------------

class cMenuSetupPluginItem : public cOsdItem {
private:
  int pluginIndex;
public:
  cMenuSetupPluginItem(const char *Name, int Index);
  int PluginIndex(void) { return pluginIndex; }
  };

cMenuSetupPluginItem::cMenuSetupPluginItem(const char *Name, int Index)
:cOsdItem(Name)
{
  pluginIndex = Index;
}

// --- cMenuSetupPlugins -----------------------------------------------------

class cMenuSetupPlugins : public cMenuSetupBase {
public:
  cMenuSetupPlugins(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetupPlugins"; }
#endif /* GRAPHTFT */
  };

cMenuSetupPlugins::cMenuSetupPlugins(void)
{
  SetSection(tr("Plugins"));
  SetHasHotkeys();
  for (int i = 0; ; i++) {
      cPlugin *p = cPluginManager::GetPlugin(i);
      if (p)
         Add(new cMenuSetupPluginItem(hk(cString::sprintf("%s (%s) - %s", p->Name(), p->Version(), p->Description())), i));
      else
         break;
      }
}

eOSState cMenuSetupPlugins::ProcessKey(eKeys Key)
{
  eOSState state = HasSubMenu() ? cMenuSetupBase::ProcessKey(Key) : cOsdMenu::ProcessKey(Key);

  if (Key == kOk) {
     if (state == osUnknown) {
        cMenuSetupPluginItem *item = (cMenuSetupPluginItem *)Get(Current());
        if (item) {
           cPlugin *p = cPluginManager::GetPlugin(item->PluginIndex());
           if (p) {
              cMenuSetupPage *menu = p->SetupMenu();
              if (menu) {
                 menu->SetPlugin(p);
                 return AddSubMenu(menu);
                 }
              Skins.Message(mtInfo, tr("This plugin has no setup parameters!"));
              }
           }
        }
     else if (state == osContinue)
        Store();
     }
  return state;
}

// --- cMenuSetup ------------------------------------------------------------

class cMenuSetup : public cOsdMenu {
private:
  virtual void Set(void);
  eOSState Restart(void);
public:
  cMenuSetup(void);
  virtual eOSState ProcessKey(eKeys Key);
#ifdef USE_GRAPHTFT
  virtual const char* MenuKind() { return "MenuSetup"; }
#endif /* GRAPHTFT */
  };

cMenuSetup::cMenuSetup(void)
:cOsdMenu("")
{
  Set();
}

void cMenuSetup::Set(void)
{
  Clear();
  char buffer[64];
  snprintf(buffer, sizeof(buffer), "%s - VDR %s", tr("Setup"), VDRVERSION);
  SetTitle(buffer);
  SetHasHotkeys();
  Add(new cOsdItem(hk(tr("OSD")),           osUser1));
  Add(new cOsdItem(hk(tr("EPG")),           osUser2));
  Add(new cOsdItem(hk(tr("DVB")),           osUser3));
  Add(new cOsdItem(hk(tr("LNB")),           osUser4));
  Add(new cOsdItem(hk(tr("CAM")),           osUser5));
  Add(new cOsdItem(hk(tr("Recording")),     osUser6));
  Add(new cOsdItem(hk(tr("Replay")),        osUser7));
  Add(new cOsdItem(hk(tr("Miscellaneous")), osUser8));
#ifdef USE_LIVEBUFFER
  Add(new cOsdItem(hk(tr("Permanent Timeshift")),osLiveBuffer));
#endif /* LIVEBUFFER */
  if (cPluginManager::HasPlugins())
  Add(new cOsdItem(hk(tr("Plugins")),       osUser9));
  Add(new cOsdItem(hk(tr("Restart")),       osUser10));
}

eOSState cMenuSetup::Restart(void)
{
  if (Interface->Confirm(tr("Really restart?")) && ShutdownHandler.ConfirmRestart(true)) {
     ShutdownHandler.Exit(1);
     return osEnd;
     }
  return osContinue;
}

eOSState cMenuSetup::ProcessKey(eKeys Key)
{
  int osdLanguage = I18nCurrentLanguage();
  eOSState state = cOsdMenu::ProcessKey(Key);

  switch (state) {
    case osUser1: return AddSubMenu(new cMenuSetupOSD);
    case osUser2: return AddSubMenu(new cMenuSetupEPG);
    case osUser3: return AddSubMenu(new cMenuSetupDVB);
    case osUser4: return AddSubMenu(new cMenuSetupLNB);
    case osUser5: return AddSubMenu(new cMenuSetupCAM);
    case osUser6: return AddSubMenu(new cMenuSetupRecord);
    case osUser7: return AddSubMenu(new cMenuSetupReplay);
    case osUser8: return AddSubMenu(new cMenuSetupMisc);
    case osUser9: return AddSubMenu(new cMenuSetupPlugins);
    case osUser10: return Restart();
#ifdef USE_LIVEBUFFER
    case osLiveBuffer: return AddSubMenu(new cMenuSetupLiveBuffer);
#endif /* LIVEBUFFER */
    default: ;
    }
  if (I18nCurrentLanguage() != osdLanguage) {
     Set();
     if (!HasSubMenu())
        Display();
     }
  return state;
}

// --- cMenuPluginItem -------------------------------------------------------

class cMenuPluginItem : public cOsdItem {
private:
  int pluginIndex;
public:
  cMenuPluginItem(const char *Name, int Index);
  int PluginIndex(void) { return pluginIndex; }
  };

cMenuPluginItem::cMenuPluginItem(const char *Name, int Index)
:cOsdItem(Name, osPlugin)
{
  pluginIndex = Index;
}

// --- cMenuMain -------------------------------------------------------------

// TRANSLATORS: note the leading and trailing blanks!
#define STOP_RECORDING trNOOP(" Stop recording ")

cOsdObject *cMenuMain::pluginOsdObject = NULL;

cMenuMain::cMenuMain(eOSState State)
:cOsdMenu("")
{
#ifdef USE_SETUP
  // Load Menu Configuration
  char *menuXML = NULL;
  asprintf(&menuXML, "%s/setup/vdr-menu.%s.xml", cPlugin::ConfigDirectory(), Setup.OSDLanguage);
  if (access(menuXML, 04) == -1)
     asprintf(&menuXML, "%s/setup/vdr-menu.xml", cPlugin::ConfigDirectory());
  subMenu.LoadXml(menuXML);
  free(menuXML);
  nrDynamicMenuEntries=0;
#endif /* SETUP */
  replaying = false;
  stopReplayItem = NULL;
  cancelEditingItem = NULL;
  stopRecordingItem = NULL;
  recordControlsState = 0;

#ifdef USE_MENUORG
  MenuOrgPatch::EnterRootMenu();
#endif /* MENUORG */

  Set();

  // Initial submenus:

#ifdef USE_MAINMENUHOOKS
  cOsdMenu *menu = NULL;
#endif /* MAINMENUHOOKS */
  switch (State) {
#ifdef USE_MAINMENUHOOKS
    case osSchedule:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osSchedule", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osSchedule: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuSchedule;
        }
        break;
    case osChannels:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osChannels", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osChannels: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuChannels;
        }
        break;
    case osTimers:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osTimers", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osTimers: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuTimers;
        }
        break;
    case osRecordings:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osRecordings", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osRecordings: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuRecordings(NULL, 0, true);
        }
        break;
    case osSetup:      menu = new cMenuSetup; break;
    case osCommands:   menu = new cMenuCommands(tr("Commands"), &Commands); break;
#else
    case osSchedule:   AddSubMenu(new cMenuSchedule); break;
    case osChannels:   AddSubMenu(new cMenuChannels); break;
    case osTimers:     AddSubMenu(new cMenuTimers); break;
    case osRecordings: AddSubMenu(new cMenuRecordings(NULL, 0, true)); break;
    case osSetup:      AddSubMenu(new cMenuSetup); break;
    case osCommands:   AddSubMenu(new cMenuCommands(tr("Commands"), &Commands)); break;
#endif /* MAINMENUHOOKS */
    default: break;
    }
#ifdef USE_MAINMENUHOOKS
  if (menu)
     AddSubMenu(menu);
#endif /* MAINMENUHOOKS */
}

cOsdObject *cMenuMain::PluginOsdObject(void)
{
  cOsdObject *o = pluginOsdObject;
  pluginOsdObject = NULL;
  return o;
}

#ifdef USE_SETUP
void cMenuMain::Set(int current)
#else
void cMenuMain::Set(void)
#endif /* SETUP */
{
  Clear();
  SetTitle("VDR");
  SetHasHotkeys();

#ifdef USE_MENUORG
  if (MenuOrgPatch::IsCustomMenuAvailable()) {
     MenuItemDefinitions* menuItems = MenuOrgPatch::MainMenuItems();
     for (MenuItemDefinitions::iterator i = menuItems->begin(); i != menuItems->end(); i++) {
         cOsdItem* osdItem = NULL;
         if ((*i)->IsCustomOsdItem()) {
            osdItem = (*i)->CustomOsdItem();
            if (osdItem &&  !(*i)->IsSeparatorItem())
               osdItem->SetText(hk(osdItem->Text()));
            }
         else if ((*i)->IsPluginItem()) {
            const char *item = (*i)->PluginMenuEntry();
            if (item)
               osdItem = new cMenuPluginItem(hk(item), (*i)->PluginIndex());
            }
         if (osdItem) {
            Add(osdItem);
            if ((*i)->IsSelected())
               SetCurrent(osdItem);
            }
         }
     }
  else {
#endif /* MENUORG */

#ifdef USE_SETUP
  stopReplayItem = NULL;
  cancelEditingItem = NULL;
  stopRecordingItem = NULL;

  // remember initial dynamic MenuEntries added
  nrDynamicMenuEntries = Count();
  for (cSubMenuNode *node = subMenu.GetMenuTree()->First(); node; node = subMenu.GetMenuTree()->Next(node)) {
      cSubMenuNode::Type type = node->GetType();
      if (type==cSubMenuNode::PLUGIN) {
         const char *item = node->GetPluginMainMenuEntry();
#ifdef USE_PINPLUGIN
         if (item && !cStatus::MsgPluginProtected(cPluginManager::GetPlugin(node->GetPluginIndex()), true))
#else
         if (item)
#endif /* PINPLUGIN */
            Add(new cMenuPluginItem(hk(item), node->GetPluginIndex()));
         }
      else if (type==cSubMenuNode::MENU) {
         char *item = NULL;
         asprintf(&item, "%s%s", node->GetName(), subMenu.GetMenuSuffix());
#ifdef USE_PINPLUGIN
         if (!cStatus::MsgMenuItemProtected(item, true))
            Add(new cOsdItem(hk(item), osUnknown, node));
#else
            Add(new cOsdItem(hk(item)));
#endif /* PINPLUGIN */
         free(item);
         }
      else if ((type==cSubMenuNode::COMMAND) || (type==cSubMenuNode::THREAD)) {
#ifdef USE_PINPLUGIN
         if (!cStatus::MsgMenuItemProtected(node->GetName(), true))
            Add(new cOsdItem(hk(node->GetName()), osUnknown, node));
#else
            Add(new cOsdItem(hk(node->GetName())));
#endif /* PINPLUGIN */
         }
      else if (type==cSubMenuNode::SYSTEM) {
         const char *item = node->GetName();
#ifdef USE_PINPLUGIN
         if (cStatus::MsgMenuItemProtected(item, true))
            ; // nothing to do ;)
         else
#endif /* PINPLUGIN */
         if (strcmp(item, "Schedule") == 0)
            Add(new cOsdItem(hk(tr("Schedule")), osSchedule));
         else if (strcmp(item, "Channels") == 0)
            Add(new cOsdItem(hk(tr("Channels")), osChannels));
         else if (strcmp(item, "Timers") == 0)
            Add(new cOsdItem(hk(tr("Timers")), osTimers));
         else if (strcmp(item, "Recordings") == 0)
            Add(new cOsdItem(hk(tr("Recordings")), osRecordings));
         else if (strcmp(item, "Setup") == 0)
            Add(new cOsdItem(hk(tr("Setup")), osSetup));
         else if (strcmp(item, "Commands") == 0 && Commands.Count()>0)
            Add(new cOsdItem(hk(tr("Commands")), osCommands));
         }
     }
  if (current >=0 && current<Count()) {
     SetCurrent(Get(current));
     }

#else /* NO SETUP */

  // Basic menu items:

#ifdef USE_PINPLUGIN
  if (!cStatus::MsgMenuItemProtected("Schedule", true))   Add(new cOsdItem(hk(tr("Schedule")),   osSchedule));
  if (!cStatus::MsgMenuItemProtected("Channels", true))   Add(new cOsdItem(hk(tr("Channels")),   osChannels));
  if (!cStatus::MsgMenuItemProtected("Timers", true))     Add(new cOsdItem(hk(tr("Timers")),     osTimers));
  if (!cStatus::MsgMenuItemProtected("Recordings", true)) Add(new cOsdItem(hk(tr("Recordings")), osRecordings));
#else
  Add(new cOsdItem(hk(tr("Schedule")),   osSchedule));
  Add(new cOsdItem(hk(tr("Channels")),   osChannels));
  Add(new cOsdItem(hk(tr("Timers")),     osTimers));
  Add(new cOsdItem(hk(tr("Recordings")), osRecordings));
#endif /* PINPLUGIN */

  // Plugins:

  for (int i = 0; ; i++) {
      cPlugin *p = cPluginManager::GetPlugin(i);
      if (p) {
#ifdef USE_PINPLUGIN
         if (!cStatus::MsgPluginProtected(p, true)) {
#endif /* PINPLUGIN */
         const char *item = p->MainMenuEntry();
         if (item)
            Add(new cMenuPluginItem(hk(item), i));
         }
#ifdef USE_PINPLUGIN
         }
#endif /* PINPLUGIN */
      else
         break;
      }

  // More basic menu items:

#ifdef USE_PINPLUGIN
  if (!cStatus::MsgMenuItemProtected("Setup", true)) Add(new cOsdItem(hk(tr("Setup")), osSetup));
#else
  Add(new cOsdItem(hk(tr("Setup")),      osSetup));
#endif /* PINPLUGIN */
  if (Commands.Count())
#ifdef USE_PINPLUGIN
     if (!cStatus::MsgMenuItemProtected("Commands", true))
#endif /* PINPLUGIN */
     Add(new cOsdItem(hk(tr("Commands")),  osCommands));

#endif /* SETUP */

#ifdef USE_MENUORG
  }
#endif /* MENUORG */

  Update(true);

  Display();
}

bool cMenuMain::Update(bool Force)
{
  bool result = false;
#ifdef USE_SETUP
  cOsdItem *fMenu = NULL;
  if (Force && subMenu.isTopMenu()) {
     fMenu = First();
     nrDynamicMenuEntries = 0;
     }

  if (subMenu.isTopMenu()) {
#endif /* SETUP */
#ifdef USE_LIEMIKUUTIO
  if (Setup.MainMenuTitle) {
     if (Setup.MainMenuTitle == 1)
        SetTitle(cString::sprintf("%s  -  %s", tr("VDR"), Setup.CustomMainMenuTitle));
     else if (Setup.MainMenuTitle == 2)
        SetTitle(cString::sprintf("%s", Setup.CustomMainMenuTitle));
     else if (Setup.MainMenuTitle == 3)
        SetTitle(cString::sprintf("%s %s", tr("VDR"), VDRVERSION));
     result = true;
     }
  else
#endif /* LIEMIKUUTIO */
  // Title with disk usage:
  if (FreeDiskSpace.HasChanged(Force)) {
     //XXX -> skin function!!!
     SetTitle(cString::sprintf("%s  -  %s", tr("VDR"), FreeDiskSpace.FreeDiskSpaceString()));
     result = true;
     }
#ifdef USE_SETUP
     }
  else {
     SetTitle(cString::sprintf("%s  -  %s", tr("VDR"), subMenu.GetParentMenuTitel()));
     result = true;
     }
#endif /* SETUP */

  bool NewReplaying = cControl::Control() != NULL;
  if (Force || NewReplaying != replaying) {
     replaying = NewReplaying;
     // Replay control:
     if (replaying && !stopReplayItem)
        // TRANSLATORS: note the leading blank!
#ifdef USE_LIEMIKUUTIO
        if (Setup.MenuCmdPosition) Ins(stopReplayItem = new cOsdItem(tr(" Stop replaying"), osStopReplay)); else
#endif /* LIEMIKUUTIO */
        Add(stopReplayItem = new cOsdItem(tr(" Stop replaying"), osStopReplay));
     else if (stopReplayItem && !replaying) {
        Del(stopReplayItem->Index());
        stopReplayItem = NULL;
        }
     // Color buttons:
     SetHelp(!replaying ? tr("Button$Record") : NULL, tr("Button$Audio"), replaying ? NULL : tr("Button$Pause"), replaying ? tr("Button$Stop") : cReplayControl::LastReplayed() ? tr("Button$Resume") : NULL);
     result = true;
     }

  // Editing control:
  bool CutterActive = cCutter::Active();
  if (CutterActive && !cancelEditingItem) {
     // TRANSLATORS: note the leading blank!
#ifdef USE_LIEMIKUUTIO
     if (Setup.MenuCmdPosition) Ins(cancelEditingItem = new cOsdItem(tr(" Cancel editing"), osCancelEdit)); else
#endif /* LIEMIKUUTIO */
     Add(cancelEditingItem = new cOsdItem(tr(" Cancel editing"), osCancelEdit));
     result = true;
     }
  else if (cancelEditingItem && !CutterActive) {
     Del(cancelEditingItem->Index());
     cancelEditingItem = NULL;
     result = true;
     }

  // Record control:
  if (cRecordControls::StateChanged(recordControlsState)) {
     while (stopRecordingItem) {
           cOsdItem *it = Next(stopRecordingItem);
           Del(stopRecordingItem->Index());
           stopRecordingItem = it;
           }
     const char *s = NULL;
     while ((s = cRecordControls::GetInstantId(s)) != NULL) {
           cOsdItem *item = new cOsdItem(osStopRecord);
           item->SetText(cString::sprintf("%s%s", tr(STOP_RECORDING), s));
#ifdef USE_LIEMIKUUTIO
           if (Setup.MenuCmdPosition) Ins(item); else
#endif /* LIEMIKUUTIO */
           Add(item);
           if (!stopRecordingItem)
              stopRecordingItem = item;
           }
     result = true;
     }

#ifdef USE_SETUP
  // adjust nrDynamicMenuEntries
  if (fMenu != NULL)
     nrDynamicMenuEntries = fMenu->Index();
#endif /* SETUP */

  return result;
}

eOSState cMenuMain::ProcessKey(eKeys Key)
{
  bool HadSubMenu = HasSubMenu();
  int osdLanguage = I18nCurrentLanguage();
  eOSState state = cOsdMenu::ProcessKey(Key);
  HadSubMenu |= HasSubMenu();

#ifdef USE_PINPLUGIN
  cOsdItem* item = Get(Current());

  if (item && item->Text() && state != osBack && state != osContinue && Key != kNone)
     if (cStatus::MsgMenuItemProtected(item->Text()))
        return osContinue;
#endif /* PINPLUGIN */

#ifdef USE_MAINMENUHOOKS
  cOsdMenu *menu = NULL;
#endif /* MAINMENUHOOKS */
  switch (state) {
#ifdef USE_MAINMENUHOOKS
    case osSchedule:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osSchedule", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osSchedule: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuSchedule;
        }
        break;
    case osChannels:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osChannels", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osChannels: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuChannels;
        }
        break;
    case osTimers:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osTimers", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osTimers: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuTimers;
        }
        break;
    case osRecordings:
        {
          cPlugin *p = cPluginManager::CallFirstService("MainMenuHooksPatch-v1.0::osRecordings", &menu);
          if (p && !menu)
             isyslog("MainMenuHook::osRecordings: plugin %s claims to support service but didn't return menu", p->Name());

          if (!menu)
             menu = new cMenuRecordings;
        }
        break;
    case osSetup:      menu = new cMenuSetup; break;
    case osCommands:   menu = new cMenuCommands(tr("Commands"), &Commands); break;
#else
    case osSchedule:   return AddSubMenu(new cMenuSchedule);
    case osChannels:   return AddSubMenu(new cMenuChannels);
    case osTimers:     return AddSubMenu(new cMenuTimers);
    case osRecordings: return AddSubMenu(new cMenuRecordings);
    case osSetup:      return AddSubMenu(new cMenuSetup);
    case osCommands:   return AddSubMenu(new cMenuCommands(tr("Commands"), &Commands));
#endif /* MAINMENUHOOKS */
    case osStopRecord: if (Interface->Confirm(tr("Stop recording?"))) {
                          cOsdItem *item = Get(Current());
                          if (item) {
                             cRecordControls::Stop(item->Text() + strlen(tr(STOP_RECORDING)));
                             return osEnd;
                             }
                          }
                       break;
    case osCancelEdit: if (Interface->Confirm(tr("Cancel editing?"))) {
                          cCutter::Stop();
                          return osEnd;
                          }
                       break;
    case osPlugin:     {
                         cMenuPluginItem *item = (cMenuPluginItem *)Get(Current());
                         if (item) {
                            cPlugin *p = cPluginManager::GetPlugin(item->PluginIndex());
                            if (p) {
#ifdef USE_PINPLUGIN
                               if (!cStatus::MsgPluginProtected(p)) {
#endif /* PINPLUGIN */
                               cOsdObject *menu = p->MainMenuAction();
                               if (menu) {
                                  if (menu->IsMenu())
                                     return AddSubMenu((cOsdMenu *)menu);
                                  else {
                                     pluginOsdObject = menu;
                                     return osPlugin;
                                     }
                                  }
                               }
                            }
#ifdef USE_PINPLUGIN
                         }
#endif /* PINPLUGIN */
                         state = osEnd;
                       }
                       break;
#ifdef USE_SETUP
    case osBack:       {
                         int newCurrent = 0;
                         if (subMenu.Up(&newCurrent)) {
                            Set(newCurrent);
                            return osContinue;
                            }
                         else
                            return osEnd;
                       }
                       break;
#endif /* SETUP */
#ifdef USE_MENUORG
    case osBack:       {
                          if (MenuOrgPatch::IsCustomMenuAvailable()) {
                             bool leavingMenuSucceeded = MenuOrgPatch::LeaveSubMenu();
                             Set();
                             stopReplayItem = NULL;
                             cancelEditingItem = NULL;
                             stopRecordingItem = NULL;
                             recordControlsState = 0;
                             Update(true);
                             Display();
                             if (leavingMenuSucceeded)
                                return osContinue;
                             else
                                return osEnd;
                             }
                       }
                       break;
    case osUser1:      {
                          if (MenuOrgPatch::IsCustomMenuAvailable()) {
                             MenuOrgPatch::EnterSubMenu(Get(Current()));
                             Set();
                             return osContinue;
                             }
                       }
                       break;
    case osUser2:      {
                          if (MenuOrgPatch::IsCustomMenuAvailable()) {
                             cOsdMenu* osdMenu = MenuOrgPatch::Execute(Get(Current()));
                             if (osdMenu)
                                return AddSubMenu(osdMenu);
                             return osEnd;
                            }
                       }
                       break;
#endif /* MENUORG */
    default: switch (Key) {
               case kRecord:
               case kRed:    if (!HadSubMenu)
                                state = replaying ? osContinue : osRecord;
                             break;
               case kGreen:  if (!HadSubMenu) {
                                cRemote::Put(kAudio, true);
                                state = osEnd;
                                }
                             break;
               case kYellow: if (!HadSubMenu)
                                state = replaying ? osContinue : osPause;
                             break;
               case kBlue:   if (!HadSubMenu)
                                state = replaying ? osStopReplay : cReplayControl::LastReplayed() ? osReplay : osContinue;
                             break;
#ifdef USE_SETUP
               case kOk:     if (state == osUnknown) {
#ifdef USE_PINPLUGIN
                                cSubMenuNode *node = Get(Current())->SubMenu();
#else
                                int index = Current()-nrDynamicMenuEntries;
                                cSubMenuNode *node = subMenu.GetNode(index);
#endif /* PINPLUGIN */

                                if (node != NULL) {
                                   if (node->GetType() == cSubMenuNode::MENU) {
#ifdef USE_PINPLUGIN
                                      subMenu.Down(node, Current());
#else
                                      subMenu.Down(index);
#endif /* PINPLUGIN */
                                      }
                                   else if (node->GetType() == cSubMenuNode::COMMAND) {
                                      char *buffer = NULL;
                                      bool confirmed = true;
                                      if (node->CommandConfirm()) {
                                         asprintf(&buffer, "%s?", node->GetName());
                                         confirmed = Interface->Confirm(buffer);
                                         free(buffer);
                                         }
                                      if (confirmed) {
                                         const char *Result = subMenu.ExecuteCommand(node->GetCommand());
                                         if (Result)
                                            return AddSubMenu(new cMenuText(node->GetName(), Result, fontFix));
                                         return osEnd;
                                         }
                                      }
                                   else if (node->GetType() == cSubMenuNode::THREAD) {
                                      char *buffer = NULL;
                                      bool confirmed = true;
                                      if (node->CommandConfirm()) {
                                         asprintf(&buffer, "%s?", node->GetName());
                                         confirmed = Interface->Confirm(buffer);
                                         free(buffer);
                                         }
                                      if (confirmed) {
                                         asprintf(&buffer, "%s", node->GetCommand());
                                         cExecCmdThread *execcmd = new cExecCmdThread(node->GetCommand());
                                         if (execcmd->Start())
                                            dsyslog("executing command '%s'", buffer);
                                         else
                                            esyslog("ERROR: can't execute command '%s'", buffer);
                                         free(buffer);
                                         return osEnd;
                                         }
                                      }
                                   }

                                Set();
                                return osContinue;
                                }
                             break;
#endif /* SETUP */
               default:      break;
               }
    }
#ifdef USE_MAINMENUHOOKS
  if (menu)
     return AddSubMenu(menu);
#endif /* MAINMENUHOOKS */
  if (!HasSubMenu() && Update(HadSubMenu))
     Display();
  if (Key != kNone) {
     if (I18nCurrentLanguage() != osdLanguage) {
        Set();
        if (!HasSubMenu())
           Display();
        }
     }
  return state;
}

// --- SetTrackDescriptions --------------------------------------------------

static void SetTrackDescriptions(int LiveChannel)
{
  cDevice::PrimaryDevice()->ClrAvailableTracks(true);
  const cComponents *Components = NULL;
  cSchedulesLock SchedulesLock;
  if (LiveChannel) {
     cChannel *Channel = Channels.GetByNumber(LiveChannel);
     if (Channel) {
        const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
        if (Schedules) {
           const cSchedule *Schedule = Schedules->GetSchedule(Channel);
           if (Schedule) {
              const cEvent *Present = Schedule->GetPresentEvent();
              if (Present)
                 Components = Present->Components();
              }
           }
        }
     }
  else if (cReplayControl::NowReplaying()) {
     cThreadLock RecordingsLock(&Recordings);
     cRecording *Recording = Recordings.GetByName(cReplayControl::NowReplaying());
     if (Recording)
        Components = Recording->Info()->Components();
     }
  if (Components) {
     int indexAudio = 0;
     int indexDolby = 0;
     int indexSubtitle = 0;
     for (int i = 0; i < Components->NumComponents(); i++) {
         const tComponent *p = Components->Component(i);
         switch (p->stream) {
           case 2: if (p->type == 0x05)
                      cDevice::PrimaryDevice()->SetAvailableTrack(ttDolby, indexDolby++, 0, LiveChannel ? NULL : p->language, p->description);
                   else
                      cDevice::PrimaryDevice()->SetAvailableTrack(ttAudio, indexAudio++, 0, LiveChannel ? NULL : p->language, p->description);
                   break;
           case 3: cDevice::PrimaryDevice()->SetAvailableTrack(ttSubtitle, indexSubtitle++, 0, LiveChannel ? NULL : p->language, p->description);
                   break;
           }
         }
     }
}

// --- cDisplayChannel -------------------------------------------------------

cDisplayChannel *cDisplayChannel::currentDisplayChannel = NULL;

cDisplayChannel::cDisplayChannel(int Number, bool Switched)
:cOsdObject(true)
{
  currentDisplayChannel = this;
  group = -1;
  withInfo = !Switched || Setup.ShowInfoOnChSwitch;
  displayChannel = Skins.Current()->DisplayChannel(withInfo);
  number = 0;
  timeout = Switched || Setup.TimeoutRequChInfo;
  channel = Channels.GetByNumber(Number);
  lastPresent = lastFollowing = NULL;
  if (channel) {
     DisplayChannel();
     DisplayInfo();
     displayChannel->Flush();
     }
  lastTime.Set();
}

cDisplayChannel::cDisplayChannel(eKeys FirstKey)
:cOsdObject(true)
{
  currentDisplayChannel = this;
  group = -1;
  number = 0;
  timeout = true;
  lastPresent = lastFollowing = NULL;
  lastTime.Set();
  withInfo = Setup.ShowInfoOnChSwitch;
  displayChannel = Skins.Current()->DisplayChannel(withInfo);
  channel = Channels.GetByNumber(cDevice::CurrentChannel());
  ProcessKey(FirstKey);
}

cDisplayChannel::~cDisplayChannel()
{
  delete displayChannel;
  cStatus::MsgOsdClear();
  currentDisplayChannel = NULL;
}

void cDisplayChannel::DisplayChannel(void)
{
  displayChannel->SetChannel(channel, number);
  cStatus::MsgOsdChannel(ChannelString(channel, number));
  lastPresent = lastFollowing = NULL;
}

void cDisplayChannel::DisplayInfo(void)
{
  if (withInfo && channel) {
     cSchedulesLock SchedulesLock;
     const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
     if (Schedules) {
        const cSchedule *Schedule = Schedules->GetSchedule(channel);
        if (Schedule) {
           const cEvent *Present = Schedule->GetPresentEvent();
           const cEvent *Following = Schedule->GetFollowingEvent();
           if (Present != lastPresent || Following != lastFollowing) {
              SetTrackDescriptions(channel->Number());
              displayChannel->SetEvents(Present, Following);
              cStatus::MsgOsdProgramme(Present ? Present->StartTime() : 0, Present ? Present->Title() : NULL, Present ? Present->ShortText() : NULL, Following ? Following->StartTime() : 0, Following ? Following->Title() : NULL, Following ? Following->ShortText() : NULL);
              lastPresent = Present;
              lastFollowing = Following;
              }
           }
        }
     }
}

void cDisplayChannel::Refresh(void)
{
  DisplayChannel();
  displayChannel->SetEvents(NULL, NULL);
}

cChannel *cDisplayChannel::NextAvailableChannel(cChannel *Channel, int Direction)
{
  if (Direction) {
     while (Channel) {
           Channel = Direction > 0 ? Channels.Next(Channel) : Channels.Prev(Channel);
#ifdef USE_PINPLUGIN
           if (cStatus::MsgChannelProtected(0, Channel) == false)
#endif /* PINPLUGIN */
#ifdef USE_LNBSHARE
           if (Channel && !Channel->GroupSep() && cDevice::GetDevice(Channel, 0, true) && cDevice::PrimaryDevice()->GetMaxBadPriority(Channel) < 0)
#else
           if (Channel && !Channel->GroupSep() && cDevice::GetDevice(Channel, 0, true))
#endif /* LNBSHARE */
              return Channel;
           }
     }
  return NULL;
}

eOSState cDisplayChannel::ProcessKey(eKeys Key)
{
  cChannel *NewChannel = NULL;
  if (Key != kNone)
     lastTime.Set();
  switch (Key) {
    case k0:
         if (number == 0) {
            // keep the "Toggle channels" function working
            cRemote::Put(Key);
            return osEnd;
            }
    case k1 ... k9:
         group = -1;
         if (number >= 0) {
            if (number > Channels.MaxNumber())
               number = Key - k0;
            else
               number = number * 10 + Key - k0;
            channel = Channels.GetByNumber(number);
            Refresh();
            withInfo = false;
            // Lets see if there can be any useful further input:
            int n = channel ? number * 10 : 0;
            int m = 10;
            cChannel *ch = channel;
            while (ch && (ch = Channels.Next(ch)) != NULL) {
                  if (!ch->GroupSep()) {
                     if (n <= ch->Number() && ch->Number() < n + m) {
                        n = 0;
                        break;
                        }
                     if (ch->Number() > n) {
                        n *= 10;
                        m *= 10;
                        }
                     }
                  }
            if (n > 0) {
               // This channel is the only one that fits the input, so let's take it right away:
               NewChannel = channel;
               withInfo = true;
               number = 0;
               Refresh();
               }
            }
         break;
    case kLeft|k_Repeat:
    case kLeft:
    case kRight|k_Repeat:
    case kRight:
#ifdef USE_VOLCTRL
         if (Setup.LRVolumeControl && !Setup.LRChannelGroups) {
            cRemote::Put(NORMALKEY(Key) == kLeft ? kVolDn : kVolUp, true);
            break;
            }
         // else fall through
#endif /* VOLCTRL */
    case kNext|k_Repeat:
    case kNext:
    case kPrev|k_Repeat:
    case kPrev:
         withInfo = false;
         number = 0;
         if (group < 0) {
            cChannel *channel = Channels.GetByNumber(cDevice::CurrentChannel());
            if (channel)
               group = channel->Index();
            }
         if (group >= 0) {
            int SaveGroup = group;
            if (NORMALKEY(Key) == kRight || NORMALKEY(Key) == kNext)
               group = Channels.GetNextGroup(group) ;
            else
               group = Channels.GetPrevGroup(group < 1 ? 1 : group);
            if (group < 0)
               group = SaveGroup;
            channel = Channels.Get(group);
            if (channel) {
               Refresh();
               if (!channel->GroupSep())
                  group = -1;
               }
            }
         break;
    case kUp|k_Repeat:
    case kUp:
    case kDown|k_Repeat:
    case kDown:
    case kChanUp|k_Repeat:
    case kChanUp:
    case kChanDn|k_Repeat:
    case kChanDn: {
         eKeys k = NORMALKEY(Key);
         cChannel *ch = NextAvailableChannel(channel, (k == kUp || k == kChanUp) ? 1 : -1);
         if (ch)
            channel = ch;
         else if (channel && channel->Number() != cDevice::CurrentChannel())
            Key = k; // immediately switches channel when hitting the beginning/end of the channel list with k_Repeat
         }
         // no break here
    case kUp|k_Release:
    case kDown|k_Release:
    case kChanUp|k_Release:
    case kChanDn|k_Release:
    case kNext|k_Release:
    case kPrev|k_Release:
         if (!(Key & k_Repeat) && channel && channel->Number() != cDevice::CurrentChannel())
            NewChannel = channel;
         withInfo = true;
         group = -1;
         number = 0;
         Refresh();
         break;
    case kNone:
         if (number && Setup.ChannelEntryTimeout && int(lastTime.Elapsed()) > Setup.ChannelEntryTimeout) {
            channel = Channels.GetByNumber(number);
            if (channel)
               NewChannel = channel;
            withInfo = true;
            number = 0;
            Refresh();
            lastTime.Set();
            }
         break;
    //TODO
    //XXX case kGreen:  return osEventNow;
    //XXX case kYellow: return osEventNext;
    case kOk:
         if (group >= 0) {
            channel = Channels.Get(Channels.GetNextNormal(group));
            if (channel)
               NewChannel = channel;
            withInfo = true;
            group = -1;
            Refresh();
            }
         else if (number > 0) {
            channel = Channels.GetByNumber(number);
            if (channel)
               NewChannel = channel;
            withInfo = true;
            number = 0;
            Refresh();
            }
         else
            return osEnd;
         break;
    default:
         if ((Key & (k_Repeat | k_Release)) == 0) {
            cRemote::Put(Key);
            return osEnd;
            }
    };
  if (!timeout || lastTime.Elapsed() < (uint64_t)(Setup.ChannelInfoTime * 1000)) {
     if (Key == kNone && !number && group < 0 && !NewChannel && channel && channel->Number() != cDevice::CurrentChannel()) {
        // makes sure a channel switch through the SVDRP CHAN command is displayed
        channel = Channels.GetByNumber(cDevice::CurrentChannel());
        Refresh();
        lastTime.Set();
        }
     DisplayInfo();
     displayChannel->Flush();
     if (NewChannel) {
        SetTrackDescriptions(NewChannel->Number()); // to make them immediately visible in the channel display
        Channels.SwitchTo(NewChannel->Number());
        SetTrackDescriptions(NewChannel->Number()); // switching the channel has cleared them
        channel = NewChannel;
        }
     return osContinue;
     }
  return osEnd;
}

// --- cDisplayVolume --------------------------------------------------------

#define VOLUMETIMEOUT 1000 //ms
#define MUTETIMEOUT   5000 //ms

cDisplayVolume *cDisplayVolume::currentDisplayVolume = NULL;

cDisplayVolume::cDisplayVolume(void)
:cOsdObject(true)
{
  currentDisplayVolume = this;
  timeout.Set(cDevice::PrimaryDevice()->IsMute() ? MUTETIMEOUT : VOLUMETIMEOUT);
  displayVolume = Skins.Current()->DisplayVolume();
  Show();
}

cDisplayVolume::~cDisplayVolume()
{
  delete displayVolume;
  currentDisplayVolume = NULL;
}

void cDisplayVolume::Show(void)
{
  displayVolume->SetVolume(cDevice::CurrentVolume(), MAXVOLUME, cDevice::PrimaryDevice()->IsMute());
}

cDisplayVolume *cDisplayVolume::Create(void)
{
  if (!currentDisplayVolume)
     new cDisplayVolume;
  return currentDisplayVolume;
}

void cDisplayVolume::Process(eKeys Key)
{
  if (currentDisplayVolume)
     currentDisplayVolume->ProcessKey(Key);
}

eOSState cDisplayVolume::ProcessKey(eKeys Key)
{
  switch (Key) {
#ifdef USE_VOLCTRL
    case kLeft|k_Repeat:
    case kLeft:
    case kRight|k_Repeat:
    case kRight:
         if (Setup.LRVolumeControl) {
            cRemote::Put(NORMALKEY(Key) == kLeft ? kVolDn : kVolUp, true);
            break;
            }
         // else fall through
#endif /* VOLCTRL */
    case kVolUp|k_Repeat:
    case kVolUp:
    case kVolDn|k_Repeat:
    case kVolDn:
         Show();
         timeout.Set(VOLUMETIMEOUT);
         break;
    case kMute:
         if (cDevice::PrimaryDevice()->IsMute()) {
            Show();
            timeout.Set(MUTETIMEOUT);
            }
         else
            timeout.Set();
         break;
    case kNone: break;
    default: if ((Key & k_Release) == 0) {
                cRemote::Put(Key);
                return osEnd;
                }
    }
  return timeout.TimedOut() ? osEnd : osContinue;
}

// --- cDisplayTracks --------------------------------------------------------

#define TRACKTIMEOUT 5000 //ms

cDisplayTracks *cDisplayTracks::currentDisplayTracks = NULL;

cDisplayTracks::cDisplayTracks(void)
:cOsdObject(true)
{
  cDevice::PrimaryDevice()->EnsureAudioTrack();
  SetTrackDescriptions(!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring() ? cDevice::CurrentChannel() : 0);
  currentDisplayTracks = this;
  numTracks = track = 0;
  audioChannel = cDevice::PrimaryDevice()->GetAudioChannel();
  eTrackType CurrentAudioTrack = cDevice::PrimaryDevice()->GetCurrentAudioTrack();
  for (int i = ttAudioFirst; i <= ttDolbyLast; i++) {
      const tTrackId *TrackId = cDevice::PrimaryDevice()->GetTrack(eTrackType(i));
      if (TrackId && TrackId->id) {
         types[numTracks] = eTrackType(i);
         descriptions[numTracks] = strdup(*TrackId->description ? TrackId->description : *TrackId->language ? TrackId->language : *itoa(i));
         if (i == CurrentAudioTrack)
            track = numTracks;
         numTracks++;
         }
      }
  descriptions[numTracks] = NULL;
  timeout.Set(TRACKTIMEOUT);
  displayTracks = Skins.Current()->DisplayTracks(tr("Button$Audio"), numTracks, descriptions);
  Show();
}

cDisplayTracks::~cDisplayTracks()
{
  delete displayTracks;
  currentDisplayTracks = NULL;
  for (int i = 0; i < numTracks; i++)
      free(descriptions[i]);
  cStatus::MsgOsdClear();
}

void cDisplayTracks::Show(void)
{
  int ac = IS_AUDIO_TRACK(types[track]) ? audioChannel : -1;
  displayTracks->SetTrack(track, descriptions);
  displayTracks->SetAudioChannel(ac);
  displayTracks->Flush();
  cStatus::MsgSetAudioTrack(track, descriptions);
  cStatus::MsgSetAudioChannel(ac);
}

cDisplayTracks *cDisplayTracks::Create(void)
{
  if (cDevice::PrimaryDevice()->NumAudioTracks() > 0) {
     if (!currentDisplayTracks)
        new cDisplayTracks;
     return currentDisplayTracks;
     }
  Skins.Message(mtWarning, tr("No audio available!"));
  return NULL;
}

void cDisplayTracks::Process(eKeys Key)
{
  if (currentDisplayTracks)
     currentDisplayTracks->ProcessKey(Key);
}

eOSState cDisplayTracks::ProcessKey(eKeys Key)
{
  int oldTrack = track;
  int oldAudioChannel = audioChannel;
  switch (Key) {
    case kUp|k_Repeat:
    case kUp:
    case kDown|k_Repeat:
    case kDown:
         if (NORMALKEY(Key) == kUp && track > 0)
            track--;
         else if (NORMALKEY(Key) == kDown && track < numTracks - 1)
            track++;
         timeout.Set(TRACKTIMEOUT);
         break;
    case kLeft|k_Repeat:
    case kLeft:
    case kRight|k_Repeat:
    case kRight: if (IS_AUDIO_TRACK(types[track])) {
                    static int ac[] = { 1, 0, 2 };
                    audioChannel = ac[cDevice::PrimaryDevice()->GetAudioChannel()];
                    if (NORMALKEY(Key) == kLeft && audioChannel > 0)
                       audioChannel--;
                    else if (NORMALKEY(Key) == kRight && audioChannel < 2)
                       audioChannel++;
                    audioChannel = ac[audioChannel];
                    timeout.Set(TRACKTIMEOUT);
                    }
         break;
    case kAudio|k_Repeat:
    case kAudio:
         if (++track >= numTracks)
            track = 0;
         timeout.Set(TRACKTIMEOUT);
         break;
    case kOk:
         if (types[track] != cDevice::PrimaryDevice()->GetCurrentAudioTrack())
            oldTrack = -1; // make sure we explicitly switch to that track
         timeout.Set();
         break;
    case kNone: break;
    default: if ((Key & k_Release) == 0)
                return osEnd;
    }
  if (track != oldTrack || audioChannel != oldAudioChannel)
     Show();
  if (track != oldTrack) {
     cDevice::PrimaryDevice()->SetCurrentAudioTrack(types[track]);
     Setup.CurrentDolby = IS_DOLBY_TRACK(types[track]);
     }
  if (audioChannel != oldAudioChannel)
     cDevice::PrimaryDevice()->SetAudioChannel(audioChannel);
  return timeout.TimedOut() ? osEnd : osContinue;
}

// --- cDisplaySubtitleTracks ------------------------------------------------

cDisplaySubtitleTracks *cDisplaySubtitleTracks::currentDisplayTracks = NULL;

cDisplaySubtitleTracks::cDisplaySubtitleTracks(void)
:cOsdObject(true)
{
  SetTrackDescriptions(!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring() ? cDevice::CurrentChannel() : 0);
  currentDisplayTracks = this;
  numTracks = track = 0;
  types[numTracks] = ttNone;
  descriptions[numTracks] = strdup(tr("No subtitles"));
  numTracks++;
  eTrackType CurrentSubtitleTrack = cDevice::PrimaryDevice()->GetCurrentSubtitleTrack();
  for (int i = ttSubtitleFirst; i <= ttSubtitleLast; i++) {
      const tTrackId *TrackId = cDevice::PrimaryDevice()->GetTrack(eTrackType(i));
      if (TrackId && TrackId->id) {
         types[numTracks] = eTrackType(i);
         descriptions[numTracks] = strdup(*TrackId->description ? TrackId->description : *TrackId->language ? TrackId->language : *itoa(i));
         if (i == CurrentSubtitleTrack)
            track = numTracks;
         numTracks++;
         }
      }
  descriptions[numTracks] = NULL;
  timeout.Set(TRACKTIMEOUT);
  displayTracks = Skins.Current()->DisplayTracks(tr("Button$Subtitles"), numTracks, descriptions);
  Show();
}

cDisplaySubtitleTracks::~cDisplaySubtitleTracks()
{
  delete displayTracks;
  currentDisplayTracks = NULL;
  for (int i = 0; i < numTracks; i++)
      free(descriptions[i]);
  cStatus::MsgOsdClear();
}

void cDisplaySubtitleTracks::Show(void)
{
  displayTracks->SetTrack(track, descriptions);
  displayTracks->Flush();
  cStatus::MsgSetSubtitleTrack(track, descriptions);
}

cDisplaySubtitleTracks *cDisplaySubtitleTracks::Create(void)
{
  if (cDevice::PrimaryDevice()->NumSubtitleTracks() > 0) {
     if (!currentDisplayTracks)
        new cDisplaySubtitleTracks;
     return currentDisplayTracks;
     }
  Skins.Message(mtWarning, tr("No subtitles available!"));
  return NULL;
}

void cDisplaySubtitleTracks::Process(eKeys Key)
{
  if (currentDisplayTracks)
     currentDisplayTracks->ProcessKey(Key);
}

eOSState cDisplaySubtitleTracks::ProcessKey(eKeys Key)
{
  int oldTrack = track;
  switch (Key) {
    case kUp|k_Repeat:
    case kUp:
    case kDown|k_Repeat:
    case kDown:
         if (NORMALKEY(Key) == kUp && track > 0)
            track--;
         else if (NORMALKEY(Key) == kDown && track < numTracks - 1)
            track++;
         timeout.Set(TRACKTIMEOUT);
         break;
    case kSubtitles|k_Repeat:
    case kSubtitles:
         if (++track >= numTracks)
            track = 0;
         timeout.Set(TRACKTIMEOUT);
         break;
    case kOk:
         if (types[track] != cDevice::PrimaryDevice()->GetCurrentSubtitleTrack())
            oldTrack = -1; // make sure we explicitly switch to that track
         timeout.Set();
         break;
    case kNone: break;
    default: if ((Key & k_Release) == 0)
                return osEnd;
    }
  if (track != oldTrack) {
     Show();
     cDevice::PrimaryDevice()->SetCurrentSubtitleTrack(types[track], true);
     }
  return timeout.TimedOut() ? osEnd : osContinue;
}

// --- cRecordControl --------------------------------------------------------

cRecordControl::cRecordControl(cDevice *Device, cTimer *Timer, bool Pause)
{
  // We're going to manipulate an event here, so we need to prevent
  // others from modifying any EPG data:
  cSchedulesLock SchedulesLock;
  cSchedules::Schedules(SchedulesLock);

  event = NULL;
  fileName = NULL;
  recorder = NULL;
  device = Device;
  if (!device) device = cDevice::PrimaryDevice();//XXX
  timer = Timer;
  if (!timer) {
     timer = new cTimer(true, Pause);
     Timers.Add(timer);
     Timers.SetModified();
     instantId = cString::sprintf(cDevice::NumDevices() > 1 ? "%s - %d" : "%s", timer->Channel()->Name(), device->CardIndex() + 1);
     }
  timer->SetPending(true);
  timer->SetRecording(true);
  event = timer->Event();

  if (event || GetEvent())
     dsyslog("Title: '%s' Subtitle: '%s'", event->Title(), event->ShortText());
  cRecording Recording(timer, event);
  fileName = strdup(Recording.FileName());

  // crude attempt to avoid duplicate recordings:
  if (cRecordControls::GetRecordControl(fileName)) {
     isyslog("already recording: '%s'", fileName);
     if (Timer) {
        timer->SetPending(false);
        timer->SetRecording(false);
        timer->OnOff();
        }
     else {
        Timers.Del(timer);
        Timers.SetModified();
        if (!cReplayControl::LastReplayed()) // an instant recording, maybe from cRecordControls::PauseLiveVideo()
           cReplayControl::SetRecording(fileName, Recording.Name());
        }
     timer = NULL;
     return;
     }

  cRecordingUserCommand::InvokeCommand(RUC_BEFORERECORDING, fileName);
  isyslog("record %s", fileName);
  if (MakeDirs(fileName, true)) {
     const cChannel *ch = timer->Channel();
#if defined (USE_LIVEBUFFER) || defined (USE_TTXTSUBS)
#ifdef USE_LIVEBUFFER
     int startFrame=-1;
     int endFrame=0;
     cLiveBuffer *liveBuffer = cLiveBufferManager::InLiveBuffer(timer, &startFrame, &endFrame);
     if (liveBuffer) {
        timer->SetFlags(tfhasLiveBuf);
        liveBuffer->SetStartFrame(startFrame);
        if (endFrame) {
           liveBuffer->CreateIndexFile(fileName, 0, endFrame);
           Timers.Del(timer);
           Timers.SetModified();
           timer = NULL;
           Recording.WriteInfo();
           Recordings.AddByName(fileName);
           return;
           }
     }
#endif /* LIVEBUFFER */
#ifdef USE_TTXTSUBS
     cTtxtSubsRecorderBase *subsRecorder = cVDRTtxtsubsHookListener::Hook()->NewTtxtSubsRecorder(device, ch);
#endif /* TTXTSUBS */
     recorder = new cRecorder(fileName, ch->GetChannelID(), timer->Priority(), ch->Vpid(), ch->Apids(), ch->Dpids(), ch->Spids()
#ifdef USE_TTXTSUBS
        ,subsRecorder
#endif /* TTXTSUBS */
#ifdef USE_LIVEBUFFER
        ,liveBuffer
#endif /* LIVEBUFFER */
        );
#else
     recorder = new cRecorder(fileName, ch->GetChannelID(), timer->Priority(), ch->Vpid(), ch->Apids(), ch->Dpids(), ch->Spids());
#endif /* LIVEBUFFER || TTXTSUBS */
     if (device->AttachReceiver(recorder)) {
#ifdef USE_TTXTSUBS
        if (subsRecorder) subsRecorder->DeviceAttach();
#endif /* TTXTSUBS */
        Recording.WriteInfo();
        cStatus::MsgRecording(device, Recording.Name(), Recording.FileName(), true);
        if (!Timer && !cReplayControl::LastReplayed()) // an instant recording, maybe from cRecordControls::PauseLiveVideo()
           cReplayControl::SetRecording(fileName, Recording.Name());
        Recordings.AddByName(fileName);
        return;
        }
     else
        DELETENULL(recorder);
     }
  if (!Timer) {
     Timers.Del(timer);
     Timers.SetModified();
     timer = NULL;
     }
}

cRecordControl::~cRecordControl()
{
  Stop();
  free(fileName);
}

#define INSTANT_REC_EPG_LOOKAHEAD 300 // seconds to look into the EPG data for an instant recording

bool cRecordControl::GetEvent(void)
{
  const cChannel *channel = timer->Channel();
  time_t Time = timer->HasFlags(tfInstant) ? timer->StartTime() + INSTANT_REC_EPG_LOOKAHEAD : timer->StartTime() + (timer->StopTime() - timer->StartTime()) / 2;
  for (int seconds = 0; seconds <= MAXWAIT4EPGINFO; seconds++) {
      {
        cSchedulesLock SchedulesLock;
        const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
        if (Schedules) {
           const cSchedule *Schedule = Schedules->GetSchedule(channel);
           if (Schedule) {
              event = Schedule->GetEventAround(Time);
              if (event) {
                 if (seconds > 0)
                    dsyslog("got EPG info after %d seconds", seconds);
                 return true;
                 }
              }
           }
      }
      if (seconds == 0)
         dsyslog("waiting for EPG info...");
      sleep(1);
      }
  dsyslog("no EPG info available");
  return false;
}

void cRecordControl::Stop(void)
{
  if (timer) {
#ifdef USE_DVLRECSCRIPTADDON
     char *chanName = NULL;
     const cChannel *recChan = NULL;

     recChan = timer -> Channel();
     if (recChan != NULL)
        chanName = strdup(recChan -> Name());
#endif /* DVLRECSCRIPTADDON */
     DELETENULL(recorder);
     timer->SetRecording(false);
     timer = NULL;
     cStatus::MsgRecording(device, NULL, fileName, false);
#ifdef USE_DVLRECSCRIPTADDON
     cRecordingUserCommand::InvokeCommand(RUC_AFTERRECORDING, fileName, chanName);
     if (chanName != NULL)
        free(chanName);
#else
     cRecordingUserCommand::InvokeCommand(RUC_AFTERRECORDING, fileName);
#endif /* DVLRECSCRIPTADDON */
     }
}

bool cRecordControl::Process(time_t t)
{
  if (!recorder || !recorder->IsAttached() || !timer || !timer->Matches(t)) {
     if (timer)
        timer->SetPending(false);
     return false;
     }
  AssertFreeDiskSpace(timer->Priority());
  return true;
}

// --- cRecordControls -------------------------------------------------------

cRecordControl *cRecordControls::RecordControls[MAXRECORDCONTROLS] = { NULL };
int cRecordControls::state = 0;

bool cRecordControls::Start(cTimer *Timer, bool Pause)
{
  static time_t LastNoDiskSpaceMessage = 0;
  int FreeMB = 0;
  if (Timer) {
     AssertFreeDiskSpace(Timer->Priority(), !Timer->Pending());
     Timer->SetPending(true);
     }
  VideoDiskSpace(&FreeMB);
  if (FreeMB < MINFREEDISK) {
     if (!Timer || time(NULL) - LastNoDiskSpaceMessage > NODISKSPACEDELTA) {
        isyslog("not enough disk space to start recording%s%s", Timer ? " timer " : "", Timer ? *Timer->ToDescr() : "");
        Skins.Message(mtWarning, tr("Not enough disk space to start recording!"));
        LastNoDiskSpaceMessage = time(NULL);
        }
     return false;
     }
  LastNoDiskSpaceMessage = 0;

  ChangeState();
  int ch = Timer ? Timer->Channel()->Number() : cDevice::CurrentChannel();
  cChannel *channel = Channels.GetByNumber(ch);

  if (channel) {
     int Priority = Timer ? Timer->Priority() : Pause ? Setup.PausePriority : Setup.DefaultPriority;
     cDevice *device = cDevice::GetDevice(channel, Priority, false);
     if (device) {
#ifdef USE_LNBSHARE
        cDevice *tmpDevice;
        while ((tmpDevice = device->GetBadDevice(channel))) {
              if (tmpDevice->Replaying() == false) {
//               Stop(tmpDevice);
                 if (tmpDevice->IsPrimaryDevice() )
                    tmpDevice->SwitchChannelForced(channel, true);
                 else
                    tmpDevice->SwitchChannelForced(channel, false);
              } else
                 tmpDevice->SwitchChannelForced(channel, false);
              }
#endif /* LNBSHARE */
        dsyslog("switching device %d to channel %d", device->DeviceNumber() + 1, channel->Number());
        if (!device->SwitchChannel(channel, false)) {
           ShutdownHandler.RequestEmergencyExit();
           return false;
           }
#ifdef USE_LIVEBUFFER
        if (!Timer || Timer->Matches() || cLiveBufferManager::InLiveBuffer(Timer)) {
#else
        if (!Timer || Timer->Matches()) {
#endif /* LIVEBUFFER */
           for (int i = 0; i < MAXRECORDCONTROLS; i++) {
               if (!RecordControls[i]) {
                  RecordControls[i] = new cRecordControl(device, Timer, Pause);
#ifdef USE_PINPLUGIN
                  cStatus::MsgRecordingFile(RecordControls[i]->FileName());
#endif /* PINPLUGIN */
                  return RecordControls[i]->Process(time(NULL));
                  }
               }
           }
        }
     else if (!Timer || (Timer->Priority() >= Setup.PrimaryLimit && !Timer->Pending())) {
        isyslog("no free DVB device to record channel %d!", ch);
        Skins.Message(mtError, tr("No free DVB device to record!"));
        }
     }
  else
     esyslog("ERROR: channel %d not defined!", ch);
  return false;
}

void cRecordControls::Stop(const char *InstantId)
{
  ChangeState();
  for (int i = 0; i < MAXRECORDCONTROLS; i++) {
      if (RecordControls[i]) {
         const char *id = RecordControls[i]->InstantId();
         if (id && strcmp(id, InstantId) == 0) {
            cTimer *timer = RecordControls[i]->Timer();
            RecordControls[i]->Stop();
            if (timer) {
               isyslog("deleting timer %s", *timer->ToDescr());
               Timers.Del(timer);
               Timers.SetModified();
               }
            break;
            }
         }
      }
}

bool cRecordControls::PauseLiveVideo(void)
{
  Skins.Message(mtStatus, tr("Pausing live video..."));
  cReplayControl::SetRecording(NULL, NULL); // make sure the new cRecordControl will set cReplayControl::LastReplayed()
  if (Start(NULL, true)) {
     sleep(2); // allow recorded file to fill up enough to start replaying
     cReplayControl *rc = new cReplayControl;
     cControl::Launch(rc);
     cControl::Attach();
     sleep(1); // allow device to replay some frames, so we have a picture
     Skins.Message(mtStatus, NULL);
     rc->ProcessKey(kPause); // pause, allowing replay mode display
     return true;
     }
  Skins.Message(mtStatus, NULL);
  return false;
}

const char *cRecordControls::GetInstantId(const char *LastInstantId)
{
  for (int i = 0; i < MAXRECORDCONTROLS; i++) {
      if (RecordControls[i]) {
         if (!LastInstantId && RecordControls[i]->InstantId())
            return RecordControls[i]->InstantId();
         if (LastInstantId && LastInstantId == RecordControls[i]->InstantId())
            LastInstantId = NULL;
         }
      }
  return NULL;
}

cRecordControl *cRecordControls::GetRecordControl(const char *FileName)
{
  for (int i = 0; i < MAXRECORDCONTROLS; i++) {
      if (RecordControls[i] && strcmp(RecordControls[i]->FileName(), FileName) == 0)
         return RecordControls[i];
      }
  return NULL;
}

void cRecordControls::Process(time_t t)
{
  for (int i = 0; i < MAXRECORDCONTROLS; i++) {
      if (RecordControls[i]) {
         if (!RecordControls[i]->Process(t)) {
            DELETENULL(RecordControls[i]);
            ChangeState();
            }
         }
      }
}

void cRecordControls::ChannelDataModified(cChannel *Channel)
{
  for (int i = 0; i < MAXRECORDCONTROLS; i++) {
      if (RecordControls[i]) {
         if (RecordControls[i]->Timer() && RecordControls[i]->Timer()->Channel() == Channel) {
            if (RecordControls[i]->Device()->ProvidesTransponder(Channel)) { // avoids retune on devices that don't really access the transponder
               isyslog("stopping recording due to modification of channel %d", Channel->Number());
               RecordControls[i]->Stop();
               // This will restart the recording, maybe even from a different
               // device in case conditional access has changed.
               ChangeState();
               }
            }
         }
      }
}

bool cRecordControls::Active(void)
{
  for (int i = 0; i < MAXRECORDCONTROLS; i++) {
      if (RecordControls[i])
         return true;
      }
  return false;
}

void cRecordControls::Shutdown(void)
{
  for (int i = 0; i < MAXRECORDCONTROLS; i++)
      DELETENULL(RecordControls[i]);
  ChangeState();
}

bool cRecordControls::StateChanged(int &State)
{
  int NewState = state;
  bool Result = State != NewState;
  State = state;
  return Result;
}

// --- cReplayControl --------------------------------------------------------

#ifdef USE_LIEMIKUUTIO
#define REPLAYCONTROLSKIPLIMIT   9    // s
#define REPLAYCONTROLSKIPSECONDS 90   // s
#define REPLAYCONTROLSKIPTIMEOUT 5000 // ms
#endif /* LIEMIKUUTIO */

cReplayControl *cReplayControl::currentReplayControl = NULL;
char *cReplayControl::fileName = NULL;
char *cReplayControl::title = NULL;

cReplayControl::cReplayControl(void)
#ifdef USE_JUMPPLAY
:cDvbPlayerControl(fileName), marks(fileName)
#else
:cDvbPlayerControl(fileName)
#endif /* JUMPPLAY */
{
  currentReplayControl = this;
  displayReplay = NULL;
  visible = modeOnly = shown = displayFrames = false;
  lastCurrent = lastTotal = -1;
  lastPlay = lastForward = false;
  lastSpeed = -2; // an invalid value
#ifdef USE_LIEMIKUUTIO
  lastSkipKey = kNone;
  lastSkipSeconds = REPLAYCONTROLSKIPSECONDS;
  lastSkipTimeout.Set(0);
#endif /* LIEMIKUUTIO */
  timeoutShow = 0;
  timeSearchActive = false;
#ifndef USE_JUMPPLAY
  marks.Load(fileName);
#endif /* JUMPPLAY */
  cRecording Recording(fileName);
#ifdef USE_DVDARCHIVE
  canJumpChapters = (Recording.GetDvdType() == DVD_VIDEO_ARCHIVE_TYPE);
  dvdchapters = NULL;
  if (canJumpChapters) {
     const char *ret = Recording.GetDvdChapters();
     if (ret)
        dvdchapters = strdup(ret);
     else
        canJumpChapters=false;
     }
#endif /* DVDARCHIVE */
  cStatus::MsgReplaying(this, Recording.Name(), Recording.FileName(), true);
  SetTrackDescriptions(false);
}

cReplayControl::~cReplayControl()
{
  Hide();
#ifdef USE_DVDARCHIVE
  free(dvdchapters);
#endif /* DVDARCHIVE */
  cStatus::MsgReplaying(this, NULL, fileName, false);
  Stop();
  if (currentReplayControl == this)
     currentReplayControl = NULL;
}

#ifdef USE_DELTIMESHIFTREC
void cReplayControl::Stop(void)
{
  int dummy;
  bool playing = GetIndex(dummy, dummy, false);
  cRecordControl* rc = cRecordControls::GetRecordControl(fileName);

  if (playing && rc && rc->InstantId()) {
     isyslog("found Timeshiftrecording");

     if ((Setup.DelTimeshiftRec != 0 ) || (Interface->Confirm(tr("Delete recording?")))) {
        cRecordControl *rc = cRecordControls::GetRecordControl(fileName);
        if (rc) {
           cTimer *timer = rc->Timer();
           if (timer) {
              const char* reccmd_backup = cRecordingUserCommand::GetCommand();
              cRecordingUserCommand::SetCommand(NULL);
 
              timer->Skip();
              cRecordControls::Process(time(NULL));
              if (timer->IsSingleEvent()) {
                 isyslog("deleting timer %s", *timer->ToDescr());
                 Timers.Del(timer);
                 }
              Timers.SetModified();

              // restore reccmd
              cRecordingUserCommand::SetCommand(reccmd_backup);
              }
           }
        isyslog("stop replaying %s", fileName);
        cDvbPlayerControl::Stop();

        if (Setup.DelTimeshiftRec != 1) {
           cRecording *recording = Recordings.GetByName(fileName);;
           if (recording) {
              if (recording->Delete()) {
                 Recordings.DelByName(fileName);
                 ClearLastReplayed(fileName);
                 return;
                 }
              else
                 Skins.Message(mtError, tr("Error while deleting recording!"));
              }
           }
        }
     else
        cDvbPlayerControl::Stop();
     }
}
#endif /* DELTIMESHIFTREC */

void cReplayControl::SetRecording(const char *FileName, const char *Title)
{
  free(fileName);
  free(title);
  fileName = FileName ? strdup(FileName) : NULL;
  title = Title ? strdup(Title) : NULL;
}

const char *cReplayControl::NowReplaying(void)
{
  return currentReplayControl ? fileName : NULL;
}

const char *cReplayControl::LastReplayed(void)
{
  return fileName;
}

void cReplayControl::ClearLastReplayed(const char *FileName)
{
  if (fileName && FileName && strcmp(fileName, FileName) == 0) {
     free(fileName);
     fileName = NULL;
     }
}

void cReplayControl::ShowTimed(int Seconds)
{
  if (modeOnly)
     Hide();
  if (!visible) {
     shown = ShowProgress(true);
     timeoutShow = (shown && Seconds > 0) ? time(NULL) + Seconds : 0;
     }
}

void cReplayControl::Show(void)
{
  ShowTimed();
}

void cReplayControl::Hide(void)
{
  if (visible) {
     delete displayReplay;
     displayReplay = NULL;
     SetNeedsFastResponse(false);
     visible = false;
     modeOnly = false;
     lastPlay = lastForward = false;
     lastSpeed = -2; // an invalid value
     timeSearchActive = false;
     }
}

void cReplayControl::ShowMode(void)
{
  if (visible || Setup.ShowReplayMode && !cOsd::IsOpen()) {
     bool Play, Forward;
     int Speed;
     if (GetReplayMode(Play, Forward, Speed) && (!visible || Play != lastPlay || Forward != lastForward || Speed != lastSpeed)) {
        bool NormalPlay = (Play && Speed == -1);

        if (!visible) {
           if (NormalPlay)
              return; // no need to do indicate ">" unless there was a different mode displayed before
           visible = modeOnly = true;
           displayReplay = Skins.Current()->DisplayReplay(modeOnly);
           }

        if (modeOnly && !timeoutShow && NormalPlay)
           timeoutShow = time(NULL) + MODETIMEOUT;
        displayReplay->SetMode(Play, Forward, Speed);
        lastPlay = Play;
        lastForward = Forward;
        lastSpeed = Speed;
        }
     }
}

bool cReplayControl::ShowProgress(bool Initial)
{
  int Current, Total;

  if (GetIndex(Current, Total) && Total > 0) {
     if (!visible) {
        displayReplay = Skins.Current()->DisplayReplay(modeOnly);
        displayReplay->SetMarks(&marks);
        SetNeedsFastResponse(true);
        visible = true;
        }
     if (Initial) {
        if (title)
           displayReplay->SetTitle(title);
#ifdef USE_OSDMAXITEMS
        displayReplay->SetButtons(tr("Jump"), tr("Skip +60s"), tr("Skip -60s"), tr("Button$Stop"));
#endif /* OSDMAXITEMS */
        lastCurrent = lastTotal = -1;
        }
     if (Total != lastTotal) {
        displayReplay->SetTotal(IndexToHMSF(Total));
        if (!Initial)
           displayReplay->Flush();
        }
     if (Current != lastCurrent || Total != lastTotal) {
        displayReplay->SetProgress(Current, Total);
        if (!Initial)
           displayReplay->Flush();
        displayReplay->SetCurrent(IndexToHMSF(Current, displayFrames));
        displayReplay->Flush();
        lastCurrent = Current;
        }
     lastTotal = Total;
     ShowMode();
     return true;
     }
  return false;
}

void cReplayControl::TimeSearchDisplay(void)
{
  char buf[64];
  // TRANSLATORS: note the trailing blank!
  strcpy(buf, tr("Jump: "));
  int len = strlen(buf);
  char h10 = '0' + (timeSearchTime >> 24);
  char h1  = '0' + ((timeSearchTime & 0x00FF0000) >> 16);
  char m10 = '0' + ((timeSearchTime & 0x0000FF00) >> 8);
  char m1  = '0' + (timeSearchTime & 0x000000FF);
  char ch10 = timeSearchPos > 3 ? h10 : '-';
  char ch1  = timeSearchPos > 2 ? h1  : '-';
  char cm10 = timeSearchPos > 1 ? m10 : '-';
  char cm1  = timeSearchPos > 0 ? m1  : '-';
  sprintf(buf + len, "%c%c:%c%c", ch10, ch1, cm10, cm1);
  displayReplay->SetJump(buf);
}

void cReplayControl::TimeSearchProcess(eKeys Key)
{
#define STAY_SECONDS_OFF_END 10
  int Seconds = (timeSearchTime >> 24) * 36000 + ((timeSearchTime & 0x00FF0000) >> 16) * 3600 + ((timeSearchTime & 0x0000FF00) >> 8) * 600 + (timeSearchTime & 0x000000FF) * 60;
  int Current = (lastCurrent / FRAMESPERSEC);
  int Total = (lastTotal / FRAMESPERSEC);
  switch (Key) {
    case k0 ... k9:
         if (timeSearchPos < 4) {
            timeSearchTime <<= 8;
            timeSearchTime |= Key - k0;
            timeSearchPos++;
            TimeSearchDisplay();
            }
         break;
    case kFastRew:
    case kLeft:
    case kFastFwd:
    case kRight: {
         int dir = ((Key == kRight || Key == kFastFwd) ? 1 : -1);
         if (dir > 0)
            Seconds = min(Total - Current - STAY_SECONDS_OFF_END, Seconds);
         SkipSeconds(Seconds * dir);
         timeSearchActive = false;
         }
         break;
    case kPlay:
    case kUp:
    case kPause:
    case kDown:
    case kOk:
         Seconds = min(Total - STAY_SECONDS_OFF_END, Seconds);
         Goto(Seconds * FRAMESPERSEC, Key == kDown || Key == kPause || Key == kOk);
         timeSearchActive = false;
         break;
    default:
         if (!(Key & k_Flags)) // ignore repeat/release keys
            timeSearchActive = false;
         break;
    }

  if (!timeSearchActive) {
     if (timeSearchHide)
        Hide();
     else
        displayReplay->SetJump(NULL);
     ShowMode();
     }
}

void cReplayControl::TimeSearch(void)
{
  timeSearchTime = timeSearchPos = 0;
  timeSearchHide = false;
  if (modeOnly)
     Hide();
  if (!visible) {
     Show();
     if (visible)
        timeSearchHide = true;
     else
        return;
     }
  timeoutShow = 0;
  TimeSearchDisplay();
  timeSearchActive = true;
}

void cReplayControl::MarkToggle(void)
{
  int Current, Total;
  if (GetIndex(Current, Total, true)) {
     cMark *m = marks.Get(Current);
     lastCurrent = -1; // triggers redisplay
     if (m)
        marks.Del(m);
     else {
        marks.Add(Current);
        ShowTimed(2);
        bool Play, Forward;
        int Speed;
#ifdef USE_JUMPPLAY
        if (GetReplayMode(Play, Forward, Speed) && !Play) {
           Goto(Current, true);
           displayFrames = true;
           }
#else
        if (GetReplayMode(Play, Forward, Speed) && !Play)
           Goto(Current, true);
#endif /* JUMPPLAY */
        }
     marks.Save();
     }
}

void cReplayControl::MarkJump(bool Forward)
{
  if (marks.Count()) {
     int Current, Total;
     if (GetIndex(Current, Total)) {
        cMark *m = Forward ? marks.GetNext(Current) : marks.GetPrev(Current);
        if (m) {
#ifdef USE_JUMPPLAY
           bool Play2, Forward2;
           int Speed;
           if (Setup.JumpPlay && GetReplayMode(Play2, Forward2, Speed) &&
              Play2 && Forward && m->position < Total - SecondsToFrames(3)) {
              Goto(m->position);
              Play();
              }
           else {
              Goto(m->position, true);
              displayFrames = true;
              }
#else
           Goto(m->position, true);
           displayFrames = true;
#endif /* JUMPPLAY */
           }
        }
     }
}

void cReplayControl::MarkMove(bool Forward)
{
  int Current, Total;
  if (GetIndex(Current, Total)) {
     cMark *m = marks.Get(Current);
     if (m) {
        displayFrames = true;
        int p = SkipFrames(Forward ? 1 : -1);
        cMark *m2;
        if (Forward) {
           if ((m2 = marks.Next(m)) != NULL && m2->position <= p)
              return;
           }
        else {
           if ((m2 = marks.Prev(m)) != NULL && m2->position >= p)
              return;
           }
        Goto(m->position = p, true);
        marks.Save();
        }
     }
}

#ifdef USE_DVDARCHIVE
void cReplayControl::ChaptersJump(bool Forward)
{
  int Current, Total;
  if (GetIndex(Current, Total)) {
     int position = -1;
     char *buf, *pos;
     cString old1("-1");
     cString old2("-1");
     buf = strdup(dvdchapters);
     pos = strtok(buf, ",");
     while (pos != NULL && position == -1) {
           if (pos && atoi(pos) > Current)
           position = Forward ? atoi(pos) : ((Current - atoi(old1)) <= (3*FRAMESPERSEC)) ? atoi(old2) : atoi(old1);
           old2 = old1;
           old1 = strdup(pos);
           if(position == -1) pos = strtok(NULL, ",");
           }
     if (!pos && !Forward)
        position = ((Current - atoi(old1)) <= (3*FRAMESPERSEC)) ? atoi(old2) : atoi(old1);
        if (position >= 0) {
           Goto(position);
           Play();
           }
        }
}

#endif /* DVDARCHIVE */
void cReplayControl::EditCut(void)
{
  if (fileName) {
     Hide();
#ifdef USE_CUTTERQUEUE
     if (!cCutter::Active() || Interface->Confirm(tr("Cutter already running - Add to cutting queue?"))) {
#else
     if (!cCutter::Active()) {
#endif /* CUTTERQUEUE */
        if (!marks.Count())
           Skins.Message(mtError, tr("No editing marks defined!"));
        else if (!cCutter::Start(fileName))
           Skins.Message(mtError, tr("Can't start editing process!"));
        else
           Skins.Message(mtInfo, tr("Editing process started"));
        }
     else
        Skins.Message(mtError, tr("Editing process already active!"));
     ShowMode();
     }
}

void cReplayControl::EditTest(void)
{
  int Current, Total;
  if (GetIndex(Current, Total)) {
     cMark *m = marks.Get(Current);
     if (!m)
        m = marks.GetNext(Current);
     if (m) {
#ifdef USE_JUMPPLAY
        if ((m->Index() & 0x01) != 0 && !Setup.PlayJump)
#else
        if ((m->Index() & 0x01) != 0)
#endif /* JUMPPLAY */
           m = marks.Next(m);
        if (m) {
           Goto(m->position - SecondsToFrames(3));
           Play();
           }
        }
     }
}

cOsdObject *cReplayControl::GetInfo(void)
{
  cRecording *Recording = Recordings.GetByName(cReplayControl::LastReplayed());
  if (Recording)
     return new cMenuRecording(Recording, false);
  return NULL;
}

eOSState cReplayControl::ProcessKey(eKeys Key)
{
  if (!Active())
     return osEnd;
#ifdef USE_JUMPPLAY
  marks.Reload();
#endif /* JUMPPLAY */
  if (visible) {
     if (timeoutShow && time(NULL) > timeoutShow) {
        Hide();
        ShowMode();
        timeoutShow = 0;
        }
     else if (modeOnly)
        ShowMode();
     else
        shown = ShowProgress(!shown) || shown;
     }
  bool DisplayedFrames = displayFrames;
  displayFrames = false;
  if (timeSearchActive && Key != kNone) {
     TimeSearchProcess(Key);
     return osContinue;
     }
#ifdef USE_DVDARCHIVE
  bool isOnMark = false;
  if (canJumpChapters) {
     int Current, Total;
     GetIndex(Current, Total);
     cMark *m = marks.Get(Current);
     if (m && (m->position == Current)) isOnMark = true;
     }
#endif /* DVDARCHIVE */
  bool DoShowMode = true;
#ifdef USE_VOLCTRL
  if (Setup.LRVolumeControl && (!Setup.LRForwardRewind || (Setup.LRForwardRewind == 1 && !visible))) {
     switch (Key) {
       // Left/Right volume control
       case kLeft|k_Repeat:
       case kLeft:
       case kRight|k_Repeat:
       case kRight:
            cRemote::Put(NORMALKEY(Key) == kLeft ? kVolDn : kVolUp, true);
            return osContinue;
            break;
       default:
            break;
       }
     }
#endif /* VOLCTRL */
  switch (Key) {
    // Positioning:
    case kPlay:
    case kUp:      Play(); break;
    case kPause:
    case kDown:    Pause(); break;
    case kFastRew|k_Release:
    case kLeft|k_Release:
                   if (Setup.MultiSpeedMode) break;
    case kFastRew:
    case kLeft:    Backward(); break;
    case kFastFwd|k_Release:
    case kRight|k_Release:
                   if (Setup.MultiSpeedMode) break;
    case kFastFwd:
    case kRight:   Forward(); break;
    case kRed:     TimeSearch(); break;
#ifdef USE_LIEMIKUUTIO
    case kGreen|k_Repeat:
    case kGreen:   SkipSeconds(-Setup.JumpSeconds); break;
    case kYellow|k_Repeat:
    case kYellow:  SkipSeconds( Setup.JumpSeconds); break;
    case k1|k_Repeat:
    case k1:       SkipSeconds(-Setup.JumpSecondsSlow); break;
    case k3|k_Repeat:
    case k3:       SkipSeconds( Setup.JumpSecondsSlow); break;
    case kPrev|k_Repeat:
    case kPrev:    if (lastSkipTimeout.TimedOut()) {
                      lastSkipSeconds = REPLAYCONTROLSKIPSECONDS;
                      lastSkipKey = kPrev;
                      }
                   else if (RAWKEY(lastSkipKey) != kPrev && lastSkipSeconds > (2 * REPLAYCONTROLSKIPLIMIT)) {
                      lastSkipSeconds /= 2;
                      lastSkipKey = kNone;
                      }
                   lastSkipTimeout.Set(REPLAYCONTROLSKIPTIMEOUT);
                   SkipSeconds(-lastSkipSeconds); break;
    case kNext|k_Repeat:
    case kNext:    if (lastSkipTimeout.TimedOut()) {
                      lastSkipSeconds = REPLAYCONTROLSKIPSECONDS;
                      lastSkipKey = kNext;	
                      }
                   else if (RAWKEY(lastSkipKey) != kNext && lastSkipSeconds > (2 * REPLAYCONTROLSKIPLIMIT)) {
                      lastSkipSeconds /= 2;
                      lastSkipKey = kNone;
                      }
                   lastSkipTimeout.Set(REPLAYCONTROLSKIPTIMEOUT);
                   SkipSeconds(lastSkipSeconds); break;
#else
    case kGreen|k_Repeat:
    case kGreen:   SkipSeconds(-60); break;
    case kYellow|k_Repeat:
    case kYellow:  SkipSeconds( 60); break;
#endif /* LIEMIKUUTIO */
    case kStop:
    case kBlue:    Hide();
                   Stop();
                   return osEnd;
#ifdef USE_DVDARCHIVE
    case kDvdChapterJumpForward|k_Repeat:
    case kDvdChapterJumpForward: if (canJumpChapters && !isOnMark) {
                                    ChaptersJump(true);
                                    }
                                 else {
                                    DoShowMode = false;
                                    MarkMove(true);
                                    }
                                 break;
    case kDvdChapterJumpBack|k_Repeat:
    case kDvdChapterJumpBack: if (canJumpChapters && !isOnMark) {
                                 ChaptersJump(false);
                                 }
                              else {
                                 DoShowMode = false;
                                 MarkMove(false);
                                 }
                              break;
#endif /* DVDARCHIVE */
    default: {
      DoShowMode = false;
      switch (Key) {
        // Editing:
        case kMarkToggle:      MarkToggle(); break;
#ifndef USE_LIEMIKUUTIO
        case kPrev|k_Repeat:
        case kPrev:
#endif /* LIEMIKUUTIO */
        case kMarkJumpBack|k_Repeat:
        case kMarkJumpBack:    MarkJump(false); break;
#ifndef USE_LIEMIKUUTIO
        case kNext|k_Repeat:
        case kNext:
#endif /* LIEMIKUUTIO */
        case kMarkJumpForward|k_Repeat:
        case kMarkJumpForward: MarkJump(true); break;
        case kMarkMoveBack|k_Repeat:
        case kMarkMoveBack:    MarkMove(false); break;
        case kMarkMoveForward|k_Repeat:
        case kMarkMoveForward: MarkMove(true); break;
        case kEditCut:         EditCut(); break;
        case kEditTest:        EditTest(); break;
        default: {
          displayFrames = DisplayedFrames;
          switch (Key) {
            // Menu control:
            case kOk:      if (visible && !modeOnly) {
                              Hide();
                              DoShowMode = true;
                              }
                           else
                              Show();
                           break;
#ifdef USE_DELTIMESHIFTREC
            case kBack: {  cRecordControl* rc = cRecordControls::GetRecordControl(fileName);
                           if (rc && rc->InstantId())
                              return osEnd;
                           else
                              return osRecordings;
                        }
#else
            case kBack:    return osRecordings;
#endif /* DELTIMESHIFTREC */
            default:       return osUnknown;
            }
          }
        }
      }
    }
  if (DoShowMode)
     ShowMode();
  return osContinue;
}
