/*
 * skinsoppalusikka.c: The 'soppalusikka' VDR skin
 *
 * $Id: $
 */

#include "common.h"
#include "tools.h"
#include "config.h"
#include "logo.h"
#include "soppalusikka.h"
#include <math.h>
#include <ctype.h>
#include <vdr/device.h>
#include <vdr/timers.h>
#include <vdr/menu.h>
#include <vdr/font.h>
#include <vdr/osd.h>
#include <vdr/themes.h>
#include <vdr/plugin.h>

#include "symbols/audio.xpm"
#include "symbols/audioleft.xpm"
#include "symbols/audioright.xpm"
#include "symbols/dolbydigital.xpm"
#include "symbols/encrypted.xpm"
#include "symbols/ffwd.xpm"
#include "symbols/ffwd1.xpm"
#include "symbols/ffwd2.xpm"
#include "symbols/ffwd3.xpm"
#include "symbols/frew.xpm"
#include "symbols/frew1.xpm"
#include "symbols/frew2.xpm"
#include "symbols/frew3.xpm"
#include "symbols/pause.xpm"
#include "symbols/play.xpm"
#include "symbols/radio.xpm"
#include "symbols/recording.xpm"
#include "symbols/running.xpm"
#include "symbols/sfwd.xpm"
#include "symbols/sfwd1.xpm"
#include "symbols/sfwd2.xpm"
#include "symbols/sfwd3.xpm"
#include "symbols/srew.xpm"
#include "symbols/srew1.xpm"
#include "symbols/srew2.xpm"
#include "symbols/srew3.xpm"
#include "symbols/teletext.xpm"
#include "symbols/vps.xpm"
#include "symbols/eventparttimer.xpm"
#include "symbols/eventtimer.xpm"
#include "symbols/eventvps.xpm"
#include "symbols/eventrunning.xpm"
#include "symbols/eventrecording.xpm"
#include "symbols/recordingnew.xpm"

static cBitmap bmAudio(audio_xpm);
static cBitmap bmAudioLeft(audioleft_xpm);
static cBitmap bmAudioRight(audioright_xpm);
static cBitmap bmDolbyDigital(dolbydigital_xpm);
static cBitmap bmEncrypted(encrypted_xpm);
static cBitmap bmFastForward(ffwd_xpm);
static cBitmap bmFastForward1(ffwd1_xpm);
static cBitmap bmFastForward2(ffwd2_xpm);
static cBitmap bmFastForward3(ffwd3_xpm);
static cBitmap bmFastReverse(frew_xpm);
static cBitmap bmFastReverse1(frew1_xpm);
static cBitmap bmFastReverse2(frew2_xpm);
static cBitmap bmFastReverse3(frew3_xpm);
static cBitmap bmPause(pause_xpm);
static cBitmap bmPlay(play_xpm);
static cBitmap bmRadio(radio_xpm);
static cBitmap bmRecording(recording_xpm);
static cBitmap bmRunning(running_xpm);
static cBitmap bmSlowForward(sfwd_xpm);
static cBitmap bmSlowForward1(sfwd1_xpm);
static cBitmap bmSlowForward2(sfwd2_xpm);
static cBitmap bmSlowForward3(sfwd3_xpm);
static cBitmap bmSlowReverse(srew_xpm);
static cBitmap bmSlowReverse1(srew1_xpm);
static cBitmap bmSlowReverse2(srew2_xpm);
static cBitmap bmSlowReverse3(srew3_xpm);
static cBitmap bmTeletext(teletext_xpm);
static cBitmap bmVPS(vps_xpm);
static cBitmap bmEventPartTimer(eventparttimer_xpm);
static cBitmap bmEventTimer(eventtimer_xpm);
static cBitmap bmEventVPS(eventvps_xpm);
static cBitmap bmEventRunning(eventrunning_xpm);
static cBitmap bmEventRecording(eventrecording_xpm);
static cBitmap bmRecordingNew(recordingnew_xpm);

static cTheme Theme;

THEME_CLR(Theme, clrBackground,             0xC8000066);
THEME_CLR(Theme, clrButtonRedFg,            0xFFFFFFFF);
THEME_CLR(Theme, clrButtonRedBg,            0xC8CC1111);
THEME_CLR(Theme, clrButtonGreenFg,          0xFF000000);
THEME_CLR(Theme, clrButtonGreenBg,          0xC822BB22);
THEME_CLR(Theme, clrButtonYellowFg,         0xFF000000);
THEME_CLR(Theme, clrButtonYellowBg,         0xC8EEEE22);
THEME_CLR(Theme, clrButtonBlueFg,           0xFFFFFFFF);
THEME_CLR(Theme, clrButtonBlueBg,           0xC82233CC);
THEME_CLR(Theme, clrMessageStatusFg,        0xFFFFEE00);
THEME_CLR(Theme, clrMessageStatusBg,        0xC8000066);
THEME_CLR(Theme, clrMessageInfoFg,          0xFFFFEE00);
THEME_CLR(Theme, clrMessageInfoBg,          0xC8000066);
THEME_CLR(Theme, clrMessageWarningFg,       0xFFFFEE00);
THEME_CLR(Theme, clrMessageWarningBg,       0xC8000066);
THEME_CLR(Theme, clrMessageErrorFg,         0xFFFFEE00);
THEME_CLR(Theme, clrMessageErrorBg,         0xC8000066);
THEME_CLR(Theme, clrVolumePromptFg,         0xFF000000);
THEME_CLR(Theme, clrVolumePromptBg,         0xFFFFFFFF);
THEME_CLR(Theme, clrVolumeBarUpper,         0xC8000066);
THEME_CLR(Theme, clrVolumeBarLower,         0xFF33CC33);
THEME_CLR(Theme, clrVolumeBarMute,          0xFFFF0000);
THEME_CLR(Theme, clrChannelNameFg,          0xFF000000);
THEME_CLR(Theme, clrChannelNameBg,          0xFFFFFFFF);
THEME_CLR(Theme, clrChannelNumberDateFg,    0xFF000000);
THEME_CLR(Theme, clrChannelNumberDateBg,    0xFFFFFFFF);
THEME_CLR(Theme, clrChannelEpgTimeFg,       0xFFDDDDDD);
THEME_CLR(Theme, clrChannelEpgTimeBg,       0xC8CC1111);
THEME_CLR(Theme, clrChannelEpgTitle,        0xFF00FFFF);
THEME_CLR(Theme, clrChannelEpgShortText,    0xFFCCBB22);
THEME_CLR(Theme, clrChannelEpgDuration,     0xFF7F7F7F);
THEME_CLR(Theme, clrChannelTimeBarFg,       0xFF7F7F7F);
THEME_CLR(Theme, clrChannelTimeBarBg,       0xFF000000);
THEME_CLR(Theme, clrChannelSymbolActive,    0xFFCCBB22);
THEME_CLR(Theme, clrChannelSymbolInactive,  0xFF7F7F7F);
THEME_CLR(Theme, clrChannelSymbolRecord,    0xFFFF0000);
THEME_CLR(Theme, clrMenuTitleFg,            0xFF000000);
THEME_CLR(Theme, clrMenuTitleBg,            0xC833AAEE);
THEME_CLR(Theme, clrMenuItemCurrentFg,      0xFF000000);
THEME_CLR(Theme, clrMenuItemCurrentBg,      0xC833AAEE);
THEME_CLR(Theme, clrMenuItemSelectable,     0xFFFFFFFF);
THEME_CLR(Theme, clrMenuEventTime,          0xFFFFFFFF);
THEME_CLR(Theme, clrMenuEventTitle,         0xFFCCBB22);
THEME_CLR(Theme, clrMenuEventShortText,     0xFFCCBB22);
THEME_CLR(Theme, clrMenuEventDescription,   0xFF00FFFF);
THEME_CLR(Theme, clrMenuEventSymbolFg,      0xFFCCBB22);
THEME_CLR(Theme, clrMenuEventSymbolBg,      0xC8000066);
THEME_CLR(Theme, clrMenuScrollbarTotal,     0xC8000066);
THEME_CLR(Theme, clrMenuScrollbarShown,     0xFFCCBB22);
THEME_CLR(Theme, clrMenuText,               0xFFCCBB22);
THEME_CLR(Theme, clrReplayTitleFg,          0xFF000000);
THEME_CLR(Theme, clrReplayTitleBg,          0xFFFFFFFF);
THEME_CLR(Theme, clrReplayCurrent,          0xFFFFFFFF);
THEME_CLR(Theme, clrReplayTotal,            0xFFFFFFFF);
THEME_CLR(Theme, clrReplayModeJump,         0xFFFFFFFF);
THEME_CLR(Theme, clrReplaySymbolActive,     0xFFCCBB22);
THEME_CLR(Theme, clrReplaySymbolInactive,   0xFF7F7F7F);
THEME_CLR(Theme, clrReplayProgressSeen,     0xFF33CC33);
THEME_CLR(Theme, clrReplayProgressRest,     0xFFFFFFFF);
THEME_CLR(Theme, clrReplayProgressSelected, 0xFFFF0000);
THEME_CLR(Theme, clrReplayProgressMark,     0xFF000000);
THEME_CLR(Theme, clrReplayProgressCurrent,  0xFFFF0000);

#define TinyGap   1
#define SmallGap  2
#define Gap       4
#define BigGap    8
#define Roundness 10

// --- cSkinSoppalusikkaDisplayChannel --------------------------------------------

class cSkinSoppalusikkaDisplayChannel : public cSkinDisplayChannel {
private:
  cOsd *osd;
  bool islogo;
  int x0, x1;
  int xt0, xt1, xt2, xt3, xt4, xt5, xt6, xt7, xt8, xt9;
  int xb0, xb1, xb2, xb3, xb4, xb5, xb6;
  int y0, y1;
  int yt0, yt1, yt2, yt3, yt4;
  int yb0, yb1, yb2, yb3, yb4, yb5;
  cString lastDate;
  bool HasChannelTimerRecording(const cChannel *Channel);
  void ResetTopAreaCoordinates(bool islogo = false);
  void DrawTopArea(const cChannel *Channel = NULL);
  void DrawBottomArea(void);
  cString GetChannelName(const cChannel *Channel);
  cString GetChannelNumber(const cChannel *Channel, int Number);
public:
  cSkinSoppalusikkaDisplayChannel(bool WithInfo);
  virtual ~cSkinSoppalusikkaDisplayChannel();
  virtual void SetChannel(const cChannel *Channel, int Number);
  virtual void SetEvents(const cEvent *Present, const cEvent *Following);
  virtual void SetMessage(eMessageType Type, const char *Text);
  virtual void Flush(void);
  };

cSkinSoppalusikkaDisplayChannel::cSkinSoppalusikkaDisplayChannel(bool WithInfo)
{
  const cFont *font = cFont::GetFont(fontOsd);
  int lineHeight = font->Height();
  islogo = false;
  // general coordinates
  x0 = 0;
  x1 = cOsd::OsdWidth();
  // top area x-coordinates
  xt9 = x1 - BigGap;
  xt0 = x0 + BigGap;
  xt1 = xt0;
  xt2 = xt1 + Roundness;
  xt3 = xt2 + font->Width("0000-");
  xt8 = xt9 - Roundness;
  xt7 = xt8 - bmTeletext.Width() - bmAudio.Width() - bmDolbyDigital.Width() - bmEncrypted.Width() - bmRecording.Width() - 7 * BigGap;
  if (SoppalusikkaConfig.showvps) {
     xt7 -= bmVPS.Width();
     }
  xt6 = xt7 - Roundness;
  xt5 = xt6 - 5 * BigGap;
  xt4 = xt5 - Roundness;
  // bottom area x-coordinates
  xb6 = x1 - BigGap;
  xb0 = x0 + BigGap;
  xb1 = xb0 + Roundness;
  xb2 = xb1 + font->Width("00:00") + BigGap;
  xb3 = xb2 + 3 * Gap;
  xb4 = xb3 + BigGap;
  xb5 = xb6 - Roundness;
  // top area y-coordinates
  y0 = 0;
  yt0 = y0 + BigGap;
  yt1 = yt0 + Roundness;
  yt2 = yt0 + lineHeight;
  yt3 = yt2 + SmallGap;
  yt4 = yt3 + lineHeight;
  // bottom area y-coordinates
  yb0 = yt4;
  yb1 = yb0 + lineHeight;
  yb2 = yb1 + lineHeight;
  yb3 = yb2 + lineHeight;
  yb5 = yb3 + lineHeight;
  yb4 = yb5 - Roundness;
  y1 = yb5;
  // create osd
  osd = cOsdProvider::NewOsd(cOsd::OsdLeft(), cOsd::OsdTop() + (Setup.ChannelInfoPos ? 0 : cOsd::OsdHeight() - y1));
  // try to use single 8bpp area
  tArea Areas[] = { { x0, y0, x1 - 1, y1 - 1, 8 } };
  if (SoppalusikkaConfig.usesinglearea && osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
     osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
  else {
     if (SoppalusikkaConfig.showlogo) {
        tArea Areas[] = { { x0, y0, x0 + ChannelLogoWidth - 1, y0 + ChannelLogoHeight - 1, 4 },
                          { x0 + ChannelLogoWidth, y0, x1 - 1, y0 + ChannelLogoHeight - 1, 4 },
                          { x0, y0 + ChannelLogoHeight, x1 - 1, y1 - 1, 4 }
                        };
        if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
           osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
        }
     else {
        tArea Areas[] = { { x0, y0, x1 - 1, y1 - 1, 4 } };
        if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
           osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
        }
     }
  // clear all
  osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
  // draw top area
  DrawTopArea(NULL);
  // draw bottom area
  DrawBottomArea();
}

cSkinSoppalusikkaDisplayChannel::~cSkinSoppalusikkaDisplayChannel()
{
  delete osd;
}

bool cSkinSoppalusikkaDisplayChannel::HasChannelTimerRecording(const cChannel *Channel)
{
  // try to find current channel from timers
  for (cTimer *t = Timers.First(); t; t = Timers.Next(t)) {
      if ((t->Channel() == Channel) && t->Recording())
         return true;
      }
  return false;
}

void cSkinSoppalusikkaDisplayChannel::ResetTopAreaCoordinates(bool islogo)
{
  const cFont *font = cFont::GetFont(fontOsd);
  // x-coordinates
  xt1 = (islogo ? x0 + ChannelLogoWidth + TinyGap : xt0);
  xt2 = xt1 + Roundness;
  xt3 = xt2 + font->Width("0000-");
}

void cSkinSoppalusikkaDisplayChannel::DrawTopArea(const cChannel *Channel)
{
  // draw logo stuff
  if (SoppalusikkaConfig.showlogo && Channel &&
     (SoppalusikkaLogoCache.Load(*Channel->GetChannelID().ToString()) || SoppalusikkaLogoCache.Load(Channel->Name()))) {
     // load channel logo
     islogo = true;
     // set top area for logo
     ResetTopAreaCoordinates(islogo);
     // draw missing bling-blings
     osd->DrawRectangle(x0, y0, x0 + TinyGap + ChannelLogoWidth - 1, y0 + SmallGap + ChannelLogoHeight - 1, Theme.Color(clrBackground));
     osd->DrawRectangle(xt0, y0 + SmallGap + ChannelLogoHeight, xt1 - 1, yt4 - 1, Theme.Color(clrChannelNameBg));
     // draw logo
     osd->DrawBitmap(x0, y0, SoppalusikkaLogoCache.Get(), 0, 0, true);
     }
  else {
     // clear possible logo leftovers
     if (islogo) {
        osd->DrawRectangle(x0, y0, xt1 - 1, yt4 - 1, clrTransparent);
        }
     // no logo
     islogo = false;
     // set top area for logoless
     ResetTopAreaCoordinates(islogo);
     }
  // draw clock row
  osd->DrawRectangle(xt1, yt0, xt4 - 1, yt2 - 1, Theme.Color(clrChannelNumberDateBg));
  osd->DrawRectangle(xt4, yt0, xt9 - 1, yt2 - 1, Theme.Color(clrBackground));
  // draw rounded left corner of clock row
  if (!islogo)
     osd->DrawEllipse(xt1, yt0, xt2 - 1, yt1 - 1, clrTransparent, -2);
  // draw rounded right edge of clock row
  osd->DrawEllipse(xt4, yt0, xt5, yt2 - 1, Theme.Color(clrChannelNumberDateBg), 5);
  // draw rounded right corner of clock row
  osd->DrawEllipse(xt8, yt0, xt9 - 1, yt1 - 1, clrTransparent, -1);
  // draw separator
  osd->DrawRectangle(xt1, yt2, xt9 - 1, yt3 - 1, Theme.Color(clrBackground));
  // draw channel row
  osd->DrawRectangle(xt1, yt3, xt6 - 1, yt4 - 1, Theme.Color(clrChannelNameBg));
  osd->DrawRectangle(xt6, yt3, xt9 - 1, yt4 - 1, Theme.Color(clrBackground));
  // draw rounded right edge of channel row
  osd->DrawEllipse(xt6, yt3, xt7, yt4 - 1, Theme.Color(clrChannelNameBg), 5);
}

void cSkinSoppalusikkaDisplayChannel::DrawBottomArea(void)
{
  // draw time area
  osd->DrawRectangle(xb0, yb0, xb2 - 1, yb5 - 1, Theme.Color(clrChannelEpgTimeBg));
  // draw rounded left corner
  osd->DrawEllipse(xb0, yb4, xb1 - 1, yb5 - 1, clrTransparent, -3);
  // draw timebar area
  osd->DrawRectangle(xb2, yb0, xb3 - 1, yb5 - 1, Theme.Color(clrChannelTimeBarBg));
  // draw event area
  osd->DrawRectangle(xb3, yb0, xb6 - 1, yb5 - 1, Theme.Color(clrBackground));
  // draw rounded right corner
  osd->DrawEllipse(xb5, yb4, xb6 - 1, yb5 - 1, clrTransparent, -4);
}

cString cSkinSoppalusikkaDisplayChannel::GetChannelName(const cChannel *Channel)
{
  char buffer[256];
  // check if channel exists
  if (Channel) {
     snprintf(buffer, sizeof(buffer), "%s", Channel->Name());
     }
  else {
     snprintf(buffer, sizeof(buffer), "%s", tr("*** Invalid Channel ***"));
     }
  return buffer;
}

cString cSkinSoppalusikkaDisplayChannel::GetChannelNumber(const cChannel *Channel, int Number)
{
  char buffer[256];
  // check if real channel exists
  if (Channel && !Channel->GroupSep()) {
     snprintf(buffer, sizeof(buffer), "%d%s", Channel->Number(), Number ? "-" : "");
     }
  else if (Number) {
     // no channel but number
     snprintf(buffer, sizeof(buffer), "%d-", Number);
     }
  else {
     // no channel and no number
     snprintf(buffer, sizeof(buffer), " ");
     }
  return buffer;
}

void cSkinSoppalusikkaDisplayChannel::SetChannel(const cChannel *Channel, int Number)
{
  // draw top area
  DrawTopArea(Channel);
  // draw channel number
  osd->DrawText(xt2, yt0, GetChannelNumber(Channel, Number), Theme.Color(clrChannelNumberDateFg), Theme.Color(clrChannelNumberDateBg), cFont::GetFont(fontSml), xt3 - xt2, yt2 - yt0);
  // draw channel name
  osd->DrawText(xt2, yt3, GetChannelName(Channel), Theme.Color(clrChannelNameFg), Theme.Color(clrChannelNameBg), cFont::GetFont(fontSml), xt6 - xt2, yt4 - yt3);
  // draw symbols
  if (Channel && !Channel->GroupSep()) {
     int xs = xt8;
     bool isvps = false;
     // check if vps
     if (SoppalusikkaConfig.showvps) {
        // get schedule
        cSchedulesLock SchedulesLock;
        const cSchedules *Schedules = cSchedules::Schedules(SchedulesLock);
        if (Schedules) {
           const cSchedule *Schedule = Schedules->GetSchedule(Channel);
           if (Schedule) {
              // get present event
              const cEvent *Event = Schedule->GetPresentEvent();
              // check if present event has vps
              if (Event && Event->Vps()) {
                 isvps = true;
                 }
              }
           }
        }
     // draw radio symbol if no video PID; otherwise draw teletext symbol
     if (Channel->Vpid()) {
        xs -= bmTeletext.Width();
        osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmTeletext.Height()) / 2, bmTeletext, Theme.Color(Channel->Tpid() ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
        }
     else {
        xs -= bmRadio.Width();
        osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmRadio.Height()) / 2, bmRadio, Theme.Color(Channel->Apid(0) ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
        }
     // draw audio symbol according to current audio channel
     switch (cDevice::PrimaryDevice()->GetAudioChannel()) {
       default:
       case 0: /* stereo */
            xs -= (bmAudio.Width() + BigGap);
            osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmAudio.Height()) / 2, bmAudio, Theme.Color(Channel->Apid(1) ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
            break;
       case 1: /* mono left */
            xs -= (bmAudioLeft.Width() + BigGap);
            osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmAudioLeft.Height()) / 2, bmAudioLeft, Theme.Color(Channel->Apid(1) ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
            break;
       case 2: /* mono right */
            xs -= (bmAudioRight.Width() + 2 * BigGap);
            osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmAudioRight.Height()) / 2, bmAudioRight, Theme.Color(Channel->Apid(1) ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
            break;
        }
     // draw dolby digital symbol
     xs -= (bmDolbyDigital.Width() + BigGap);
     osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmDolbyDigital.Height()) / 2, bmDolbyDigital, Theme.Color(Channel->Dpid(0) ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
     // draw encryption symbol
     xs -= (bmEncrypted.Width() + BigGap);
     osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmEncrypted.Height()) / 2, bmEncrypted, Theme.Color(Channel->Ca() ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
     // draw vps symbol
     if (SoppalusikkaConfig.showvps) {
        xs -= (bmVPS.Width() + BigGap);
        osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmVPS.Height()) / 2, bmVPS, Theme.Color(isvps ? clrChannelSymbolActive : clrChannelSymbolInactive), Theme.Color(clrBackground));
        }
     // draw recording symbol
     xs -= (bmRecording.Width() + BigGap);
     osd->DrawBitmap(xs, yt0 + (yt4 - yt0 - bmRecording.Height()) / 2, bmRecording, Theme.Color(cRecordControls::Active() ? (HasChannelTimerRecording(Channel) ? clrChannelSymbolRecord : clrChannelSymbolActive) : clrChannelSymbolInactive), Theme.Color(clrBackground));
     }
  lastDate = NULL;
}

void cSkinSoppalusikkaDisplayChannel::SetEvents(const cEvent *Present, const cEvent *Following)
{
  // draw bottom area
  DrawBottomArea();
  // check epg datas
  const cEvent *e = Present;
  if (e) {
     cString s;
     int total = e->Duration();
     int now = (time(NULL) - e->StartTime());
     if ((now < total) && ((now / 60) > 0))
        s = cString::sprintf("  %d / %d %s", now / 60, total / 60, tr("min"));
     else
        s = cString::sprintf("  %d %s", total / 60, tr("min"));
     // draw start time
     osd->DrawText(xb1, yb0, e->GetTimeString(), Theme.Color(clrChannelEpgTimeFg), Theme.Color(clrChannelEpgTimeBg), cFont::GetFont(fontOsd), xb2 - xb1, yb1 - yb0);
     // draw title
     osd->DrawText(xb4, yb0, e->Title(), Theme.Color(clrChannelEpgTitle), Theme.Color(clrBackground), cFont::GetFont(fontOsd), xb5 - xb4, yb1 - yb0);
     // draw duration
     osd->DrawText(xb5 - cFont::GetFont(fontSml)->Width(s), yb0, s, Theme.Color(clrChannelEpgDuration), Theme.Color(clrBackground), cFont::GetFont(fontSml), cFont::GetFont(fontSml)->Width(s), yb1 - yb0);
     // draw vps time
     if (SoppalusikkaConfig.showvps && e->Vps() && (e->Vps() != e->StartTime())) {
        /* difference between start time and vps time in minutes */
        int delta = (e->StartTime() - e->Vps()) / 60;
        /* check if difference is less than 10 hours */
        if (abs(delta) < 600) {
           /* relative vps time formats: "+0:30" "-1:30" */
           cString vps = cString::sprintf("%c%01d:%02d", delta < 0 ? '-' : '+', abs(delta) / 60, abs(delta) % 60);
           osd->DrawText(xb1, yb1, vps, Theme.Color(clrChannelEpgShortText), Theme.Color(clrChannelEpgTimeBg), cFont::GetFont(fontSml), xb2 - xb1 - Gap, yb2 - yb1, taRight);
           }
        else {
           /* absolute vps time format: "18:45" */
           osd->DrawText(xb1, yb1, TimeString(e->Vps()), Theme.Color(clrChannelEpgShortText), Theme.Color(clrChannelEpgTimeBg), cFont::GetFont(fontSml), xb2 - xb1 - Gap, yb2 - yb1, taRight);
           }
        }
     // draw shorttext
     osd->DrawText(xb4, yb1, e->ShortText(), Theme.Color(clrChannelEpgShortText), Theme.Color(clrBackground), cFont::GetFont(fontSml), xb5 - xb4 - Gap, yb2 - yb1);
     // draw timebar
     int yc = yb0 + (int)(roundf((float)(now) / (float)(total) * (float)(yb5 - yb0)));
     yc = min(yc, yb5);
     osd->DrawRectangle(xb2 + SmallGap, yb0, xb3 - SmallGap - 1, yc - 1, Theme.Color(clrChannelTimeBarFg));
     }
  e = Following;
  if (e) {
     cString s = cString::sprintf("  %d %s", e->Duration() / 60, tr("min"));
     // draw start time
     osd->DrawText(xb1, yb2, e->GetTimeString(), Theme.Color(clrChannelEpgTimeFg), Theme.Color(clrChannelEpgTimeBg), cFont::GetFont(fontOsd), xb2 - xb1, yb3 - yb2);
     // draw title
     osd->DrawText(xb4, yb2, e->Title(), Theme.Color(clrChannelEpgTitle), Theme.Color(clrBackground), cFont::GetFont(fontOsd), xb5 - xb4, yb3 - yb2);
     // draw duration
     osd->DrawText(xb5 - cFont::GetFont(fontSml)->Width(*s), yb2, s, Theme.Color(clrChannelEpgDuration), Theme.Color(clrBackground), cFont::GetFont(fontSml), cFont::GetFont(fontSml)->Width(s), yb3 - yb2);
     // draw vps time - only if skin dependent small fonts
     if (SoppalusikkaConfig.showvps && e->Vps() && (e->Vps() != e->StartTime())) {
        /* difference between start time and vps time in minutes */
        int delta = (e->StartTime() - e->Vps()) / 60;
        /* check if difference is less than 10 hours */
        if (abs(delta) < 600) {
           /* relative vps time formats: "+0:30" "-1:30" */
           cString vps = cString::sprintf("%c%d:%02d", delta < 0 ? '-' : '+', abs(delta) / 60, abs(delta) % 60);
           osd->DrawText(xb1, yb3, vps, Theme.Color(clrChannelEpgShortText), Theme.Color(clrChannelEpgTimeBg), cFont::GetFont(fontSml), xb2 - xb1 - Gap, yb5 - yb3, taRight);
           }
        else {
           /* absolute vps time format: "18:45" */
           osd->DrawText(xb1, yb3, TimeString(e->Vps()), Theme.Color(clrChannelEpgShortText), Theme.Color(clrChannelEpgTimeBg), cFont::GetFont(fontSml), xb2 - xb1 - Gap, yb5 - yb3, taRight);
           }
        }
     // draw shorttext
     osd->DrawText(xb4, yb3, e->ShortText(), Theme.Color(clrChannelEpgShortText), Theme.Color(clrBackground), cFont::GetFont(fontSml), xb5 - xb4, yb5 - yb3);
     }
}

void cSkinSoppalusikkaDisplayChannel::SetMessage(eMessageType Type, const char *Text)
{
  // check if message
  if (Text) {
     // save osd region
     osd->SaveRegion(xb0, yb3, xb6 - 1, yb5 - 1);
     // draw message area
     osd->DrawRectangle(xb0, yb3, xb6 - 1, yb5 - 1, Theme.Color(clrMessageStatusBg + 2 * Type));
     // draw message
     osd->DrawText(xb1, yb3, Text, Theme.Color(clrMessageStatusFg + 2 * Type), Theme.Color(clrMessageStatusBg + 2 * Type), cFont::GetFont(fontOsd), xb5 - xb1, yb5 - yb3, taCenter);
     // draw rounded left corner
     osd->DrawEllipse(xb0, yb4, xb1 - 1, yb5 - 1, clrTransparent, -3);
     // draw rounded right corner
     osd->DrawEllipse(xb5, yb4, xb6 - 1, yb5 - 1, clrTransparent, -4);
     }
  else {
     // restore saved osd region
     osd->RestoreRegion();
     }
}

void cSkinSoppalusikkaDisplayChannel::Flush(void)
{
  cString date = DayDateTime();
  if (!*lastDate || strcmp(date, lastDate)) {
     // draw updated date string
     osd->DrawText(xt3, yt0, date, Theme.Color(clrChannelNumberDateFg), Theme.Color(clrChannelNumberDateBg), cFont::GetFont(fontSml), xt4 - xt3, yt2 - yt0, taRight);
     lastDate = date;
     }
  osd->Flush();
}

// --- cSkinSoppalusikkaDisplayMenu -----------------------------------------------

class cSkinSoppalusikkaDisplayMenu : public cSkinDisplayMenu {
private:
  cOsd *osd;
  int x0, x1, x2, x3, x4, x5;
  int y0, y1, y2, y3, y4, y5, y6, y7, y8;
  int lineHeight;
  bool lastRec;
  cString lastDate;
  void DrawScrollbar(int Total, int Offset, int Shown, int Top, int Height, bool CanScrollUp, bool CanScrollDown);
  void SetTextScrollbar(void);
public:
  cSkinSoppalusikkaDisplayMenu();
  virtual ~cSkinSoppalusikkaDisplayMenu();
  virtual void Scroll(bool Up, bool Page);
  virtual int MaxItems(void);
  virtual void Clear(void);
  virtual void SetTitle(const char *Title);
  virtual void SetButtons(const char *Red, const char *Green = NULL, const char *Yellow = NULL, const char *Blue = NULL);
  virtual void SetMessage(eMessageType Type, const char *Text);
  virtual void SetItem(const char *Text, int Index, bool Current, bool Selectable);
  virtual void SetScrollbar(int Total, int Offset);
  virtual void SetEvent(const cEvent *Event);
  virtual void SetRecording(const cRecording *Recording);
  virtual void SetText(const char *Text, bool FixedFont);
  virtual int GetTextAreaWidth(void) const;
  virtual const cFont *GetTextAreaFont(bool FixedFont) const;
  virtual void Flush(void);
  };

cSkinSoppalusikkaDisplayMenu::cSkinSoppalusikkaDisplayMenu(void)
{
  const cFont *font = cFont::GetFont(fontOsd);
  lineHeight = font->Height();
  lastRec = false;
  x0 = 0;
  x1 = x0 + Gap;
  x2 = x0 + Roundness;
  x5 = cOsd::OsdWidth();
  x4 = x5 - Gap;
  x3 = x5 - Roundness;
  y0 = 0;
  y1 = y0 + Roundness;
  y2 = y0 + lineHeight;
  y3 = y2 + SmallGap;
  y4 = y3 + lineHeight;
  y8 = cOsd::OsdHeight();
  y7 = y8 - Roundness;
  y6 = y8 - lineHeight;
  y5 = y6 - lineHeight;
  // create osd
  osd = cOsdProvider::NewOsd(cOsd::OsdLeft(), cOsd::OsdTop());
  // try to use single 8bpp area
  tArea Areas[] = { { x0, y0, x5 - 1, y8 - 1, 8 } };
  if (SoppalusikkaConfig.usesinglearea && osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk) {
     osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
     // clear all
     osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
     }
  else {
     tArea Areas[] = { { x0, y0, x5 - 1, y8 - 1, 4 } };
     if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk) {
        osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
        // clear all
        osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
        }
     else {
        tArea Areas[] = { { x0, y0, x5 - 1, y2 - 1, 2 },
                          { x0, y2, x5 - 1, y5 - 1, 2 },
                          { x0, y5, x5 - 1, y6 - 1, 2 },
                          { x0, y6, x5 - 1, y8 - 1, 4 }
                        };
        if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
           osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
        // clear all
        osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
        // fill up items area palette to prevent palette overflow
        osd->GetBitmap(1)->Reset();
        osd->GetBitmap(1)->SetColor(0, Theme.Color(clrBackground));
        osd->GetBitmap(1)->SetColor(1, Theme.Color(clrMenuItemSelectable));
        osd->GetBitmap(1)->SetColor(2, Theme.Color(clrMenuItemCurrentBg));
        osd->GetBitmap(1)->SetColor(3, Theme.Color(clrMenuItemCurrentFg));
        }
     }
  // draw titlebar
  osd->DrawRectangle(x0, y0, x5 - 1, y2 - 1, Theme.Color(clrMenuTitleBg));
  // draw rounded left corner of title bar
  osd->DrawEllipse(x0, y0, x2 - 1, y1 - 1, clrTransparent, -2);
  // draw rounded right corner of title bar
  osd->DrawEllipse(x3, y0, x5 - 1, y1 - 1, clrTransparent, -1);
  // draw gap + items + message area
  osd->DrawRectangle(x0, y2, x5 - 1, y6 - 1, Theme.Color(clrBackground));
  // draw colorbar
  osd->DrawRectangle(x0, y6, x5 - 1, y8 - 1, Theme.Color(clrBackground));
  osd->DrawRectangle(x0, y6, x2 - 1, y8 - 1, Theme.Color(clrButtonRedBg));
  osd->DrawRectangle(x3, y6, x5 - 1, y8 - 1, Theme.Color(clrButtonBlueBg));
  SetButtons(NULL);
  // draw rounded left corner of colorbar
  osd->DrawEllipse(x0, y7, x2 - 1, y8 - 1, clrTransparent, -3);
  // draw rounded right corner of colorbar
  osd->DrawEllipse(x3, y7, x5 - 1, y8 - 1, clrTransparent, -4);
}

cSkinSoppalusikkaDisplayMenu::~cSkinSoppalusikkaDisplayMenu()
{
  delete osd;
}

void cSkinSoppalusikkaDisplayMenu::DrawScrollbar(int Total, int Offset, int Shown, int Top, int Height, bool CanScrollUp, bool CanScrollDown)
{
  // check if scrollbar is needed
  if (Total > 0 && Total > Shown) {
     int yt = Top;
     int yb = yt + Height;
     int st = yt;
     int sb = yb;
     int th = max(int((sb - st) * double(Shown) / Total + 0.5), Gap);
     int tt = min(int(st + (sb - st) * double(Offset) / Total + 0.5), sb - th);
     int tb = min(tt + th, sb);
     int xl = x5 - Gap;
     // draw background of scrollbar
     osd->DrawRectangle(xl, st, x5 - 1, sb - 1, Theme.Color(clrMenuScrollbarTotal));
     // draw visible area of scrollbar
     osd->DrawRectangle(xl, tt, x5 - 1, tb - 1, Theme.Color(clrMenuScrollbarShown));
     }
}

void cSkinSoppalusikkaDisplayMenu::SetTextScrollbar(void)
{
  if (textScroller.CanScroll())
     DrawScrollbar(textScroller.Total(), textScroller.Offset(), textScroller.Shown(), textScroller.Top(), textScroller.Height(), textScroller.CanScrollUp(), textScroller.CanScrollDown());
}

void cSkinSoppalusikkaDisplayMenu::Scroll(bool Up, bool Page)
{
  cSkinDisplayMenu::Scroll(Up, Page);
  SetTextScrollbar();
}

int cSkinSoppalusikkaDisplayMenu::MaxItems(void)
{
  // max number of items
  return (y5 - y3) / lineHeight;
}

void cSkinSoppalusikkaDisplayMenu::Clear(void)
{
  textScroller.Reset();
  // clear items area
  osd->DrawRectangle(x0, y2, x5 - 1, y6 - 1, Theme.Color(clrBackground));
}

void cSkinSoppalusikkaDisplayMenu::SetTitle(const char *Title)
{
  const cFont *font = cFont::GetFont(fontSml);
  int w = Gap + font->Width("Wmm 07.07 07:07") + (cRecordControls::Active() ?  bmRecording.Width() : 0);
  // draw title
  osd->DrawText(x2, y0, Title, Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg), cFont::GetFont(fontSml), x3 - x2 - w, y2 - y0);
}

void cSkinSoppalusikkaDisplayMenu::SetButtons(const char *Red, const char *Green, const char *Yellow, const char *Blue)
{
  const cFont *font = cFont::GetFont(fontSml);
  int t0 = x2;
  int t4 = x3;
  int t2 = t0 + (t4 - t0) / 2;
  int t1 = t0 + (t2 - t0) / 2;
  int t3 = t2 + (t4 - t2) / 2;
  // draw color buttons
  osd->DrawText(t0, y6, Red,    Theme.Color(clrButtonRedFg),    Theme.Color(clrButtonRedBg),    font, t1 - t0, y8 - y6, taCenter);
  osd->DrawText(t1, y6, Green,  Theme.Color(clrButtonGreenFg),  Theme.Color(clrButtonGreenBg),  font, t2 - t1, y8 - y6, taCenter);
  osd->DrawText(t2, y6, Yellow, Theme.Color(clrButtonYellowFg), Theme.Color(clrButtonYellowBg), font, t3 - t2, y8 - y6, taCenter);
  osd->DrawText(t3, y6, Blue,   Theme.Color(clrButtonBlueFg),   Theme.Color(clrButtonBlueBg),   font, t4 - t3, y8 - y6, taCenter);
}

void cSkinSoppalusikkaDisplayMenu::SetMessage(eMessageType Type, const char *Text)
{
  // check if message
  if (Text) {
     // draw message
     osd->DrawText(x2, y5, Text, Theme.Color(clrMessageStatusFg + 2 * Type), Theme.Color(clrMessageStatusBg + 2 * Type), cFont::GetFont(fontOsd), x4 - x2, y6 - y5, taCenter);
     }
  else {
     // clear message area
     osd->DrawRectangle(x2, y5, x4 - 1, y6 - 1, Theme.Color(clrBackground));
     }
}

void cSkinSoppalusikkaDisplayMenu::SetItem(const char *Text, int Index, bool Current, bool Selectable)
{
  int y = y3 + Index * lineHeight;
  tColor ColorFg, ColorBg;
  // select colors
  if (Current) {
     ColorFg = Theme.Color(clrMenuItemCurrentFg);
     ColorBg = Theme.Color(clrMenuItemCurrentBg);
     }
  else {
     ColorFg = Theme.Color(Selectable ? clrMenuItemSelectable : clrMenuItemCurrentBg);
     ColorBg = Theme.Color(clrBackground);
     }
  const cFont *font = cFont::GetFont(fontOsd);
  // draw rounded left corner
  osd->DrawEllipse(x1, y, x2 - 1, y + lineHeight - 1, ColorBg, 7);
  // draw item
  for (int i = 0; i < MaxTabs; i++) {
      const char *s = GetTabbedText(Text, i);
      if (s) {
         char buffer[9];
         int xt = x2 + Tab(i);
         bool iseventinfo = false;
         bool isnewrecording = false;
         bool isprogressbar = false;
         int now = 0, total = 0;
         // check if event info symbol: "tTV*" "R"
         if (SoppalusikkaConfig.showsymbols) {
            // check if event info characters
            if (strlen(s) == 3 && ischaracter(s[0], " tTR") && ischaracter(s[1], " V") && ischaracter(s[2], " *")) {
               // update status
               iseventinfo = true;
               }
            }
         // check if new recording: "01.01.06*", "10:10*"
         if (!iseventinfo && SoppalusikkaConfig.showsymbols &&
            (strlen(s) == 6 && s[5] == '*' && s[2] == ':' && isdigit(*s) && isdigit(*(s + 1)) && isdigit(*(s + 3)) && isdigit(*(s + 4))) ||
            (strlen(s) == 9 && s[8] == '*' && s[5] == '.' && s[2] == '.' && isdigit(*s) && isdigit(*(s + 1)) && isdigit(*(s + 3)) && isdigit(*(s + 4)) && isdigit(*(s + 6)) && isdigit(*(s + 7)))) {
            // update status
            isnewrecording = true;
            // make a copy
            strncpy(buffer, s, strlen(s));
            // remove the '*' character
            buffer[strlen(s) - 1] = '\0';
            }
         // check if progress bar: "[|||||||   ]"
         if (!iseventinfo && !isnewrecording && SoppalusikkaConfig.showprogressbar &&
            (strlen(s) > 5 && s[0] == '[' && s[strlen(s) - 1] == ']')) {
            const char *p = s + 1;
            // update status
            isprogressbar = true;
            for (; *p != ']'; ++p) {
                // check if progressbar characters
                if (*p == ' ' || *p == '|') {
                   // update counters
                   ++total;
                   if (*p == '|')
                      ++now;
                   }
                else {
                   // wrong character detected; not a progressbar
                   isprogressbar = false;
                   break;
                   }
                }
            }
         if (iseventinfo) {
            int evx = xt + Gap;
            const char *p = s;
            // draw background
            osd->DrawRectangle(xt, y, x3, y + lineHeight - 1, ColorBg);
            // draw symbols
            for (; *p; ++p) {
                switch (*p) {
                  case 't':
                       // partial timer event
                       osd->DrawBitmap(evx, y + (lineHeight - bmEventPartTimer.Height()) / 2, bmEventPartTimer, ColorFg, ColorBg);
                       evx += bmEventPartTimer.Width();
                       break;
                  case 'T':
                       // timer event
                       osd->DrawBitmap(evx, y + (lineHeight - bmEventTimer.Height()) / 2, bmEventTimer, ColorFg, ColorBg);
                       evx += bmEventTimer.Width();
                       break;
                  case 'R':
                       // recording event (epgsearch)
                       osd->DrawBitmap(evx, y + (lineHeight - bmEventRecording.Height()) / 2, bmEventRecording, ColorFg, ColorBg);
                       evx += bmEventRecording.Width();
                       break;
                  case 'V':
                       // vps event
                       osd->DrawBitmap(evx, y + (lineHeight - bmEventVPS.Height()) / 2, bmEventVPS, ColorFg, ColorBg);
                       evx += bmEventVPS.Width();
                       break;
                  case '*':
                       // running event
                       osd->DrawBitmap(evx, y + (lineHeight - bmEventRunning.Height()) / 2, bmEventRunning, ColorFg, ColorBg);
                       evx += bmEventRunning.Width();
                       break;
                  case ' ':
                  default:
                       // let's ignore space character
                       break;
                  }
                }
            }
         else if (isnewrecording) {
            // draw text
            osd->DrawText(xt, y, buffer, ColorFg, ColorBg, font, x3 - xt);
            // draw symbol
            osd->DrawBitmap(xt + font->Width(buffer), y + (lineHeight - bmRecordingNew.Height()) / 2, bmRecordingNew, ColorFg, ColorBg);
            }
         else if (isprogressbar) {
            // define x coordinates of progressbar
            int px0 = xt;
            int px1 = px0 + SmallGap;
            int px2 = px1 + SmallGap;
            int px5;
            if (Tab(i+1))
                px5 = px0 + Tab(i+1) - Tab(i);
            else
                px5 = px0 + x2 - x1 - Tab(i);
            int px4 = px5 - SmallGap;
            int px3 = px4 - SmallGap;
            int px = px2 + (int)((float)now * (float)(px3 - px2) / (float)total);
            // define y coordinates of progressbar
            int py0 = y + Gap;
            int py1 = py0 + SmallGap;
            int py2 = py1 + Gap;
            int py5 = y + lineHeight - Gap;
            int py4 = py5 - SmallGap;
            int py3 = py4 - Gap;
            // draw background
            osd->DrawRectangle(xt, y, x3, y + lineHeight - 1, ColorBg);
            // draw progressbar
            osd->DrawRectangle(px0, py0, px1, py5, ColorFg);
            osd->DrawRectangle(px4, py0, px5, py5, ColorFg);
            osd->DrawRectangle(px1, py0, px4, py1, ColorFg);
            osd->DrawRectangle(px1, py4, px4, py5, ColorFg);
            osd->DrawRectangle(px2, py2, px,  py3, ColorFg);
            }
         else {
            // draw text
            osd->DrawText(xt, y, s, ColorFg, ColorBg, font, x3 - xt);
            }
         }
      if (!Tab(i + 1))
         break;
      }
  // draw rounded right corner
  osd->DrawEllipse(x3, y, x4 - 1, y + lineHeight - 1, ColorBg, 5);
  //set editable width
  SetEditableWidth(x3 - x2 - Tab(1));
}

void cSkinSoppalusikkaDisplayMenu::SetScrollbar(int Total, int Offset)
{
  DrawScrollbar(Total, Offset, MaxItems(), y2, MaxItems() * lineHeight, Offset > 0, Offset + MaxItems() < Total);
}

void cSkinSoppalusikkaDisplayMenu::SetEvent(const cEvent *Event)
{
  // check if event
  if (!Event)
     return;
  const cFont *font = cFont::GetFont(fontOsd);
  const cFont *smlfont = cFont::GetFont(fontSml);
  cTextScroller ts;
  cString date;
  int y = y3;
  int xs = x2;
  // check if event has timer
  if (Event->HasTimer()) {
     // draw timer symbol
     osd->DrawBitmap(xs, y + (y4 - y) / bmRecording.Height(), bmRecording, Theme.Color(clrMenuEventSymbolFg), Theme.Color(clrMenuEventSymbolBg));
     xs += (bmRecording.Width() + Roundness);
     }
  // check if event is running
  if (Event->IsRunning()) {
     // draw running symbol
     osd->DrawBitmap(xs, y + (y4 - y) / bmRunning.Height(), bmRunning, Theme.Color(clrMenuEventSymbolFg), Theme.Color(clrMenuEventSymbolBg));
     xs += (bmRunning.Width() + Roundness);
     }
  // check if event has VPS 
  if (Event->Vps()) {
     // draw VPS symbol
     osd->DrawBitmap(xs, y + (y4 - y) / bmVPS.Height(), bmVPS, Theme.Color(clrMenuEventSymbolFg), Theme.Color(clrMenuEventSymbolBg));
     xs += (bmVPS.Width() + Roundness);
     }
  y = y4;
  // draw event date / duration string
  date = cString::sprintf("%s  %s - %s (%d %s)", *Event->GetDateString(), *Event->GetTimeString(), *Event->GetEndTimeString(), Event->Duration() / 60, tr("min"));
  ts.Set(osd, x2, y, x3 - x2, y5 - y, *date, font, Theme.Color(clrMenuEventTime), Theme.Color(clrBackground));
  y += ts.Height();
  // check if event has VPS
  if (Event->Vps() && Event->Vps() != Event->StartTime()) {
     cString buffer = cString::sprintf("%s: %s", tr("VPS"), *Event->GetVpsString());
     ts.Set(osd, x2, y, x3 - x2, y5 - y, *buffer, smlfont, Theme.Color(clrMenuEventTime), Theme.Color(clrBackground));
     y += ts.Height();
     }
  // draw event languages
  const cComponents *Components = Event->Components();
  if (Components) {
     cString info, audio, subtitle;
     unsigned int numaudio = 0, numsubtitle = 0;
     for (int i = 0; i < Components->NumComponents(); i++) {
         const tComponent *p = Components->Component(i);
         if (p->language) {
            if (p->stream == 2) {
               audio = cString::sprintf("%s%s%s%s%s%s",
                                       (numaudio > 0) ? *audio : "",
                                       (numaudio > 0) ? ", " : "",
                                       p->description ? p->description : p->language,
                                       p->description ? " (" : "",
                                       p->description ? p->language : "",
                                       p->description ? ")" : "");
               numaudio++;
               }
            else if (p->stream == 3) {
               subtitle = cString::sprintf("%s%s%s (%s%s%s)",
                                        (numsubtitle > 0) ? *subtitle : "",
                                        (numsubtitle > 0) ? ", " : "",
                                        p->description ? p->description : p->language,
                                        p->description ? p->language : "",
                                        p->description ? "; " : "",
                                        (p->type < 0x10) ? tr("EBU") : trVDR("DVB"));
               numsubtitle++;
               }
            }
         }
     if (numaudio > 0)
        info = cString::sprintf("%s: %s", trVDR("Setup.DVB$Audio languages"), *audio);
     if (numsubtitle > 0)
        info = cString::sprintf("%s%s%s: %s", *info ? *info : "", *info ? "\n" : "", trVDR("Setup.DVB$Subtitle languages"), *subtitle);
     if (*info) {
        ts.Set(osd, x2, y, x3 - x2, y5 - y, *info, smlfont, Theme.Color(clrMenuEventTime), Theme.Color(clrBackground));
        y += ts.Height();
        }
     }
  y += smlfont->Height();
  // draw event title
  ts.Set(osd, x2, y, x3 - x2, y5 - y, Event->Title(), font, Theme.Color(clrMenuEventTitle), Theme.Color(clrBackground));
  y += ts.Height();
  // draw recording short text and description
  if (isempty(Event->Description())) {
     y += smlfont->Height();
     // check if short text
     if (!isempty(Event->ShortText())) {
        // draw short text as description, if no description available
        textScroller.Set(osd, x2, y, x3 - x2, y5 - y, Event->ShortText(), font, Theme.Color(clrMenuEventDescription), Theme.Color(clrBackground));
        SetTextScrollbar();
        }
     }
  else {
     // check if short text
     if (!isempty(Event->ShortText())) {
        // draw short text
        ts.Set(osd, x2, y, x3 - x2, y5 - y, Event->ShortText(), smlfont, Theme.Color(clrMenuEventShortText), Theme.Color(clrBackground));
        y += ts.Height();
        }
     y += smlfont->Height();
     // draw description
     textScroller.Set(osd, x2, y, x3 - x2, y5 - y, Event->Description(), font, Theme.Color(clrMenuEventDescription), Theme.Color(clrBackground));
     SetTextScrollbar();
     }
}

void cSkinSoppalusikkaDisplayMenu::SetRecording(const cRecording *Recording)
{
  // check if recording
  if (!Recording)
     return;
  const cRecordingInfo *Info = Recording->Info();
  const cFont *font = cFont::GetFont(fontOsd);
  const cFont *smlfont = cFont::GetFont(fontSml);
  int y = y3;
  cTextScroller ts;
  char t[32];
  // draw recording date string
  snprintf(t, sizeof(t), "%s  %s", *DateString(Recording->start), *TimeString(Recording->start));
  ts.Set(osd, x2, y, x3 - x2, y5 - y, t, font, Theme.Color(clrMenuEventTime), Theme.Color(clrBackground));
  y += ts.Height();
  // draw additional information
  cString info = cString::sprintf("%s: %d %s: %d", tr("Priority"), Recording->priority, tr("Lifetime"), Recording->lifetime);
  if (SoppalusikkaConfig.showauxinfo && Info->Aux()) {
     char *aux = strdup(Info->Aux());
     info = cString::sprintf("%s\n%s: %s", *info, tr("Auxiliary information"), parseaux(aux));
     free(aux);
     }
  const cComponents *Components = Info->Components();
  if (Components) {
     cString audio, subtitle;
     unsigned int numaudio = 0, numsubtitle = 0;
     for (int i = 0; i < Components->NumComponents(); i++) {
         const tComponent *p = Components->Component(i);
         if (p->language) {
            if (p->stream == 2) {
               audio = cString::sprintf("%s%s%s%s%s%s",
                                       (numaudio > 0) ? *audio : "",
                                       (numaudio > 0) ? ", " : "",
                                       p->description ? p->description : p->language,
                                       p->description ? " (" : "",
                                       p->description ? p->language : "",
                                       p->description ? ")" : "");
               numaudio++;
               }
            else if (p->stream == 3) {
               subtitle = cString::sprintf("%s%s%s (%s%s%s)",
                                        (numsubtitle > 0) ? *subtitle : "",
                                        (numsubtitle > 0) ? ", " : "",
                                        p->description ? p->description : p->language,
                                        p->description ? p->language : "",
                                        p->description ? "; " : "",
                                        (p->type < 0x10) ? tr("EBU") : trVDR("DVB"));
               numsubtitle++;
               }
            }
         }
     if (numaudio > 0)
        info = cString::sprintf("%s\n%s: %s", *info, trVDR("Setup.DVB$Audio languages"), *audio);
     if (numsubtitle > 0)
        info = cString::sprintf("%s\n%s: %s", *info, trVDR("Setup.DVB$Subtitle languages"), *subtitle);
     }
  ts.Set(osd, x2, y, x3 - x2, y5 - y, *info, smlfont, Theme.Color(clrMenuEventTime), Theme.Color(clrBackground));
  y += ts.Height();
  y += smlfont->Height();
  // draw recording title
  const char *Title = Info->Title();
  if (isempty(Title))
     Title = Recording->Name();
  ts.Set(osd, x2, y, x3 - x2, y5 - y, Title, font, Theme.Color(clrMenuEventTitle), Theme.Color(clrBackground));
  y += ts.Height();
  // draw recording short text and description
  if (isempty(Info->Description())) {
     y += smlfont->Height();
     // check if short text
     if (!isempty(Info->ShortText())) {
        // draw short text as description, if no description available
        textScroller.Set(osd, x2, y, x3 - x2, y5 - y, Info->ShortText(), font, Theme.Color(clrMenuEventDescription), Theme.Color(clrBackground));
        SetTextScrollbar();
        }
     }
  else {
     // check if short text
     if (!isempty(Info->ShortText())) {
        // draw short text
        ts.Set(osd, x2, y, x3 - x2, y5 - y, Info->ShortText(), smlfont, Theme.Color(clrMenuEventShortText), Theme.Color(clrBackground));
        y += ts.Height();
        }
     y += smlfont->Height();
     // draw description
     textScroller.Set(osd, x2, y, x3 - x2, y5 - y, Info->Description(), font, Theme.Color(clrMenuEventDescription), Theme.Color(clrBackground));
     SetTextScrollbar();
     }
}

void cSkinSoppalusikkaDisplayMenu::SetText(const char *Text, bool FixedFont)
{
  // draw text
  textScroller.Set(osd, x2, y3, x3 - x2, y5 - y3, Text, GetTextAreaFont(FixedFont), Theme.Color(clrMenuText), Theme.Color(clrBackground));
  SetTextScrollbar();
}

int cSkinSoppalusikkaDisplayMenu::GetTextAreaWidth(void) const
{
  // max text area width
  return x3 - x2;
}

const cFont *cSkinSoppalusikkaDisplayMenu::GetTextAreaFont(bool FixedFont) const
{
  // text area font
  return cFont::GetFont(FixedFont ? fontFix : fontOsd);
}

void cSkinSoppalusikkaDisplayMenu::Flush(void)
{
  cString date = DayDateTime();
  const cFont *font = cFont::GetFont(fontSml);
  int w = font->Width("Wmm 07.07 07:07");
  bool rec = cRecordControls::Active();
  if (lastRec != rec) {
     int xl = x3 - w - bmRecording.Width();
     // draw safety margin
     osd->DrawRectangle(xl - Gap, y0, xl - 1, y2 - 1, Theme.Color(clrMenuTitleBg));
     // draw recording symbol
     osd->DrawBitmap(xl, y0 + (y2 - y0 - bmRecording.Height()) / 2, bmRecording, Theme.Color(clrMenuTitleBg), Theme.Color(rec ? clrMenuTitleFg : clrMenuTitleBg));
     lastRec = rec;
     }
  if (!*lastDate || strcmp(date, lastDate)) {
     // update date string on titlebar
     osd->DrawText(x3 - w, y0, date, Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg), font, w, y2 - y0, taRight);
     lastDate = date;
     }
  osd->Flush();
}

// --- cSkinSoppalusikkaDisplayReplay ---------------------------------------------

class cSkinSoppalusikkaDisplayReplay : public cSkinDisplayReplay {
private:
  cOsd *osd;
  int x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10;
  int y0, y1, y2, y3, y4, y5;
  int lineHeight;
  bool drawdate;
  bool modeonly;
  cString lastDate;
public:
  cSkinSoppalusikkaDisplayReplay(bool ModeOnly);
  virtual ~cSkinSoppalusikkaDisplayReplay();
  virtual void SetTitle(const char *Title);
  virtual void SetMode(bool Play, bool Forward, int Speed);
  virtual void SetProgress(int Current, int Total);
  virtual void SetCurrent(const char *Current);
  virtual void SetTotal(const char *Total);
  virtual void SetJump(const char *Jump);
  virtual void SetMessage(eMessageType Type, const char *Text);
  virtual void Flush(void);
  };

cSkinSoppalusikkaDisplayReplay::cSkinSoppalusikkaDisplayReplay(bool ModeOnly)
{
  int sw = bmFastReverse.Width() + bmSlowReverse.Width() + bmPlay.Width() + bmPause.Width() + bmSlowForward.Width() + bmFastForward.Width();
  lineHeight = cFont::GetFont(fontOsd)->Height();
  drawdate = true;
  modeonly = ModeOnly;
  x0 = 0;
  x1 = x0 + BigGap;
  x2 = x1 + Roundness;
  x3 = x2 + sw + 5 * Gap;
  x4 = x3 + 4 * BigGap;
  x10 = cOsd::OsdWidth();
  x9 = x10 - BigGap;
  x8 = x9 - Roundness;
  x6 = x8 - sw - 5 * Gap;
  x7 = x6 + (x8 - x6) / 2;
  x5 = x6 - 4 * BigGap;
  y0 = 0;
  y1 = y0 + Roundness;
  y2 = y0 + lineHeight + Gap;
  y3 = y2 + lineHeight;
  y5 = y3 + lineHeight;
  y4 = y5 - Roundness;
  // create osd
  osd = cOsdProvider::NewOsd(cOsd::OsdLeft(), cOsd::OsdTop() + cOsd::OsdHeight() - y5);
  // try to use single 8bpp area
  tArea Areas[] = { { x0, y0, x10 - 1, y5 - 1, 8 } };
  if (SoppalusikkaConfig.usesinglearea && osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
     osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
  else {
     tArea Areas[] = { { x0, y0, x10 - 1, y5 - 1, 4 } };
     if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
        osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
     }
  // clear all
  osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
  // select mode
  if (modeonly) {
     // draw statusbar
     osd->DrawRectangle(x6, y3, x8 - 1, y5 - 1, Theme.Color(clrBackground));
     // draw rounded left corner
     osd->DrawEllipse(x6 - Roundness - 1, y3, x6 - 1, y5 - 1, Theme.Color(clrBackground), 7);
     // draw rounded right corner
     osd->DrawEllipse(x8, y3, x9 - 1, y5 - 1, Theme.Color(clrBackground), 5);
     }
  else {
     // draw titlebar
     osd->DrawRectangle(x0, y0, x5 - 1, y2 - 1, Theme.Color(clrReplayTitleFg));
     // draw rounded left corner of timebar
     osd->DrawSlope(x5, y0, x6 - 1, y2 - 1, Theme.Color(clrBackground), 0);
     // draw timebar
     osd->DrawRectangle(x6, y0, x9 - 1, y2 - 1, Theme.Color(clrBackground));
     osd->DrawText(x7 - Roundness / 2, y0, "/", Theme.Color(clrReplayCurrent), Theme.Color(clrBackground), cFont::GetFont(fontSml), Roundness, y2 - y0, taCenter);
     // draw rounded right corner of timebar
     osd->DrawEllipse(x8, y0, x9 - 1, y1 - 1, clrTransparent, -1);
     // draw progressbar
     osd->DrawRectangle(x1, y2, x9 - 1, y3 - 1, Theme.Color(clrBackground));
     // draw statusbar
     osd->DrawRectangle(x1, y3, x3 - 1, y5 - 1, Theme.Color(clrBackground));
     osd->DrawRectangle(x6, y3, x9 - 1, y5 - 1, Theme.Color(clrBackground));
     // draw rounded center area of statusbar
     osd->DrawSlope(x3, y3, x4 - 1, y5 - 1, Theme.Color(clrBackground), 1);
     osd->DrawSlope(x5, y3, x6 - 1, y5 - 1, Theme.Color(clrBackground), 3);
     // draw rounded left corner of statusbar
     osd->DrawEllipse(x1, y4, x2 - 1, y5 - 1, clrTransparent, -3);
     // draw rounded right corner of statusbar
     osd->DrawEllipse(x8, y4, x9 - 1, y5 - 1, clrTransparent, -4);
     }
}

cSkinSoppalusikkaDisplayReplay::~cSkinSoppalusikkaDisplayReplay()
{
  delete osd;
}

void cSkinSoppalusikkaDisplayReplay::SetTitle(const char *Title)
{
  // draw titlebar
  osd->DrawText(x0 + SmallGap, y0 + SmallGap, Title, Theme.Color(clrReplayTitleFg), Theme.Color(clrReplayTitleBg), cFont::GetFont(fontSml), x5 - x0 - 2 * SmallGap, y2 - y0 - 2 * SmallGap);
}

void cSkinSoppalusikkaDisplayReplay::SetMode(bool Play, bool Forward, int Speed)
{
  cBitmap *bm = NULL;
  // start drawing from right corner
  int xs = x8;
  // clamp parameters
  if (Speed < -1)
     Speed = -1;
  if (Speed > 3)
     Speed = 3;
  // check if current mode is fast forward
  if ((Speed > -1) && Play && Forward) {
     // select symbol
     switch (Speed) {
       case 0:
            bm = &bmFastForward;
            break;
       case 1:
            bm = &bmFastForward1;
            break;
       case 2:
            bm = &bmFastForward2;
            break;
       default:
       case 3:
            bm = &bmFastForward3;
            break;
       }
     // draw active symbol
     xs -= bm->Width();
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bm->Height()) / 2, *bm, Theme.Color(clrReplaySymbolActive), Theme.Color(clrBackground));
     }
  else {
     // draw inactive symbol
     xs -= bmFastForward.Width();
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmFastForward.Height()) / 2, bmFastForward, Theme.Color(clrReplaySymbolInactive), Theme.Color(clrBackground));
     }
  // check if current mode is slow forward
  if ((Speed > -1) && !Play && Forward) {
     // select symbol
     switch (Speed) {
       case 0:
            bm = &bmSlowForward;
            break;
       case 1:
            bm = &bmSlowForward1;
            break;
       case 2:
            bm = &bmSlowForward2;
            break;
       default:
       case 3:
            bm = &bmSlowForward3;
            break;
       }
     // draw active symbol
     xs -= bm->Width() + Gap;
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bm->Height()) / 2, *bm, Theme.Color(clrReplaySymbolActive), Theme.Color(clrBackground));
     }
  else {
     // draw inactive symbol
     xs -= bmSlowForward.Width() + Gap;
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmSlowForward.Height()) / 2, bmSlowForward, Theme.Color(clrReplaySymbolInactive), Theme.Color(clrBackground));
     }
  // check if current mode is pause
  xs -= bmPause.Width() + Gap;
  if ((Speed == -1) && !Play) {
     // draw active symbol
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmPause.Height()) / 2, bmPause, Theme.Color(clrReplaySymbolActive), Theme.Color(clrBackground));
     }
  else {
     // draw inactive symbol
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmPause.Height()) / 2, bmPause, Theme.Color(clrReplaySymbolInactive), Theme.Color(clrBackground));
     }
  // check if current mode is play
  xs -= bmPlay.Width() + Gap;
  if ((Speed == -1) && Play) {
     // draw active symbol
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmPlay.Height()) / 2, bmPlay, Theme.Color(clrReplaySymbolActive), Theme.Color(clrBackground));
     }
  else {
     // draw inactive symbol
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmPlay.Height()) / 2, bmPlay, Theme.Color(clrReplaySymbolInactive), Theme.Color(clrBackground));
     }
  // check if current mode is slow reverse
  if ((Speed > -1) && !Play && !Forward) {
     // select symbol
     switch (Speed) {
       case 0:
            bm = &bmSlowReverse;
            break;
       case 1:
            bm = &bmSlowReverse1;
            break;
       case 2:
            bm = &bmSlowReverse2;
            break;
       default:
       case 3:
            bm = &bmSlowReverse3;
            break;
       }
     // draw active symbol
     xs -= bm->Width() + Gap;
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bm->Height()) / 2, *bm, Theme.Color(clrReplaySymbolActive), Theme.Color(clrBackground));
     }
  else {
     // draw inactive symbol
     xs -= bmSlowReverse.Width() + Gap;
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmSlowReverse.Height()) / 2, bmSlowReverse, Theme.Color(clrReplaySymbolInactive), Theme.Color(clrBackground));
     }
  // check if current mode is fast reverse
  if ((Speed > -1) && Play && !Forward) {
     // select symbol
     switch (Speed) {
       case 0:
            bm = &bmFastReverse;
            break;
       case 1:
            bm = &bmFastReverse1;
            break;
       case 2:
            bm = &bmFastReverse2;
            break;
       default:
       case 3:
            bm = &bmFastReverse3;
            break;
       }
     // draw active symbol
     xs -= bm->Width() + Gap;
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bm->Height()) / 2, *bm, Theme.Color(clrReplaySymbolActive), Theme.Color(clrBackground));
     }
  else {
     // draw inactive symbol
     xs -= bmFastReverse.Width() + Gap;
     osd->DrawBitmap(xs, y3 + (y5 - y3 - bmFastReverse.Height()) / 2, bmFastReverse, Theme.Color(clrReplaySymbolInactive), Theme.Color(clrBackground));
     }
}

void cSkinSoppalusikkaDisplayReplay::SetProgress(int Current, int Total)
{
  // create progressbar
  cProgressBar pb(x9 - x1 - 2 * SmallGap, y3 - y2 - 2 * SmallGap, Current, Total, marks, Theme.Color(clrReplayProgressSeen), Theme.Color(clrReplayProgressRest), Theme.Color(clrReplayProgressSelected), Theme.Color(clrReplayProgressMark), Theme.Color(clrReplayProgressCurrent));
  // draw progressbar
  osd->DrawBitmap(x1 + SmallGap, y2 + SmallGap, pb);
}

void cSkinSoppalusikkaDisplayReplay::SetCurrent(const char *Current)
{
  // draw current time
  osd->DrawText(x6, y0, Current, Theme.Color(clrReplayCurrent), Theme.Color(clrBackground), cFont::GetFont(fontSml), x7 - x6 - BigGap, y2 - y0, taRight);
}

void cSkinSoppalusikkaDisplayReplay::SetTotal(const char *Total)
{
  // draw total time
  osd->DrawText(x7 + BigGap, y0, Total, Theme.Color(clrReplayTotal), Theme.Color(clrBackground), cFont::GetFont(fontSml), x8 - x7 - BigGap, y2 - y0, taLeft);
}

void cSkinSoppalusikkaDisplayReplay::SetJump(const char *Jump)
{
  // check if prompt
  if (Jump) {
     // disallow date updating
     drawdate = false;
     }
  else {
     // allow date updating
     drawdate = true;
     lastDate = NULL;
     }
  // draw jump prompt
  osd->DrawText(x2, y3, Jump, Theme.Color(clrReplayModeJump), Theme.Color(clrBackground), cFont::GetFont(fontOsd), x3 - x2, y5 - y3, taCenter);
}

void cSkinSoppalusikkaDisplayReplay::SetMessage(eMessageType Type, const char *Text)
{
  // check if new message
  if (Text) {
     // disallow date updating
     drawdate = false;
     // save current osd
     osd->SaveRegion(x0, y3, x10 - 1, y5 - 1);
     // draw statusbar
     osd->DrawRectangle(x0, y3, x1 - 1, y5 - 1, clrTransparent);
     osd->DrawRectangle(x1, y3, x9 - 1, y5 - 1, Theme.Color(clrMessageStatusBg + 2 * Type));
     osd->DrawRectangle(x9, y3, x10 - 1, y5 - 1, clrTransparent);
     // draw rounded left corner
     osd->DrawEllipse(x1, y3, x2 - 1, y5 - 1, Theme.Color(clrMessageStatusBg + 2 * Type), 7);
     // draw message
     osd->DrawText(x2, y3, Text, Theme.Color(clrMessageStatusFg + 2 * Type), Theme.Color(clrMessageStatusBg + 2 * Type), cFont::GetFont(fontOsd), x8 - x2, y5 - y3, taCenter);
     // draw rounded right corner
     osd->DrawEllipse(x8, y3, x9 - 1, y5 - 1, Theme.Color(clrMessageStatusBg + 2 * Type), 5);
     }
  else {
     // restore saved osd
     osd->RestoreRegion();
     // allow date updating
     drawdate = true;
     lastDate = NULL;
     }
}

void cSkinSoppalusikkaDisplayReplay::Flush(void)
{
  if (drawdate && !modeonly) {
     cString date = DayDateTime();
     if (!*lastDate || strcmp(date, lastDate)) {
        // update date
        osd->DrawText(x2, y3, date, Theme.Color(clrReplayModeJump), Theme.Color(clrBackground), cFont::GetFont(fontSml), x3 - x2, y5 - y3, taCenter);
        lastDate = date;
        }
     }
  osd->Flush();
}

// --- cSkinSoppalusikkaDisplayVolume ---------------------------------------------

class cSkinSoppalusikkaDisplayVolume : public cSkinDisplayVolume {
private:
  cOsd *osd;
  int x0, x1, x2, x3, x4, x5;
  int y0, y1, y2, y3;
  int lineHeight;
public:
  cSkinSoppalusikkaDisplayVolume();
  virtual ~cSkinSoppalusikkaDisplayVolume();
  virtual void SetVolume(int Current, int Total, bool Mute);
  virtual void Flush(void);
  };

cSkinSoppalusikkaDisplayVolume::cSkinSoppalusikkaDisplayVolume()
{
  lineHeight = cFont::GetFont(fontSml)->Height();
  x0 = 0;
  x1 = x0 + BigGap;
  x2 = x1 + Roundness;
  x5 = cOsd::OsdWidth();
  x4 = x5 - BigGap;
  x3 = x4 - Roundness;
  y0 = 0;
  y1 = y0 + lineHeight;
  y2 = y1 + Gap;
  y3 = y2 + lineHeight;
  // create osd
  osd = cOsdProvider::NewOsd(cOsd::OsdLeft(), cOsd::OsdTop() + cOsd::OsdHeight() - y3);
  // try to use single 8bpp area
  tArea Areas[] = { { x0, y0, x5 - 1, y3 - 1, 8 } };
  if (SoppalusikkaConfig.usesinglearea && osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
     osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
  else {
     tArea Areas[] = { { x0, x0, x5 - 1, y3 - 1, 4 } };
     if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
        osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
     }
  // clear all
  osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
}

cSkinSoppalusikkaDisplayVolume::~cSkinSoppalusikkaDisplayVolume()
{
  delete osd;
}

void cSkinSoppalusikkaDisplayVolume::SetVolume(int Current, int Total, bool Mute)
{
  tColor ColorBar;
  const char *Prompt;
  const cFont *font = cFont::GetFont(fontSml);

  // select behaviour
  if (Mute) {
     ColorBar = Theme.Color(clrVolumeBarMute);
     Prompt = tr("Mute");
     }
  else {
     ColorBar = Theme.Color(clrVolumeBarLower);
     Prompt = tr("Volume");
     }
  // width of active titlebar
  int wfg = x0 + font->Width(Prompt) + 2 * Gap;
  int wbg = wfg + 2 * Gap;
  // current volume
  int vol = x2 + (x3 - x2) * Current / Total;
  // draw titlebar
  osd->DrawRectangle(x0, y0, wbg - 1, y2 - 1, Theme.Color(clrVolumePromptFg));
  osd->DrawRectangle(wbg, y0, x4 - 1, y2 - 1, clrTransparent);
  osd->DrawText(x0 + Gap, y0 + Gap, Prompt, Theme.Color(clrVolumePromptFg), Theme.Color(clrVolumePromptBg), font, wfg, lineHeight, taCenter);
  // draw rounded left corner
  osd->DrawEllipse(x1, y2, x2 - 1, y3 - 1, Theme.Color(clrBackground), 3);
  // draw volumebar
  osd->DrawRectangle(x2, y2, x3 - 1,  y3 - 1, Theme.Color(clrBackground));
  osd->DrawRectangle(x2, y2 + Gap, vol, y3 - Gap - 1, ColorBar);
  osd->DrawRectangle(vol + 1, y2 + Gap, x3 - 1, y3 - Gap - 1, Theme.Color(clrVolumeBarUpper));
  // draw rounded right corner
  osd->DrawEllipse(x3, y2, x4 - 1, y3 - 1, Theme.Color(clrBackground), 5);
}

void cSkinSoppalusikkaDisplayVolume::Flush(void)
{
  osd->Flush();
}

// --- cSkinSoppalusikkaDisplayTracks ---------------------------------------------

class cSkinSoppalusikkaDisplayTracks : public cSkinDisplayTracks {
private:
  cOsd *osd;
  int x0, x1, x2, x3, x4, x5, x6;
  int y0, y1, y2, y3, y4, y5, y6, y7;
  int lineHeight;
  int currentIndex;
  void SetItem(const char *Text, int Index, bool Current);
public:
  cSkinSoppalusikkaDisplayTracks(const char *Title, int NumTracks, const char * const *Tracks);
  virtual ~cSkinSoppalusikkaDisplayTracks();
  virtual void SetTrack(int Index, const char * const *Tracks);
  virtual void SetAudioChannel(int AudioChannel);
  virtual void Flush(void);
  };

cSkinSoppalusikkaDisplayTracks::cSkinSoppalusikkaDisplayTracks(const char *Title, int NumTracks, const char * const *Tracks)
{
  lineHeight = cFont::GetFont(fontOsd)->Height();
  currentIndex = -1;
  const cFont *font = cFont::GetFont(fontSml);
  int ItemsWidth = font->Width(Title);
  for (int i = 0; i < NumTracks; i++)
      ItemsWidth = max(ItemsWidth, font->Width(Tracks[i]));
  ItemsWidth = max(ItemsWidth, font->Width(tr("Stereo")));
  ItemsWidth = max(ItemsWidth, font->Width(tr("Left")));
  ItemsWidth = max(ItemsWidth, font->Width(tr("Right")));
  ItemsWidth += 2 * BigGap + 2 * bmAudio.Width();
  x0 = 0;
  x1 = x0 + Roundness;
  x2 = x1 + ItemsWidth;
  x3 = x2 + BigGap;
  x4 = x3 + bmAudio.Width();
  x5 = x4 + Roundness;
  x6 = cOsd::OsdWidth();
  y0 = 0;
  y1 = y0 + Roundness;
  y3 = y0 + lineHeight;
  y2 = y3 - Gap;
  y4 = y3 + NumTracks * lineHeight;
  y5 = y4 + Gap;
  y7 = y5 + lineHeight;
  y6 = y7 - Roundness;
  // create osd
  osd = cOsdProvider::NewOsd(cOsd::OsdLeft(), cOsd::OsdTop() + cOsd::OsdHeight() - y7);
  // try to use single 8bpp area
  tArea Areas[] = { { x0, y0, x6 - 1, y7 - 1, 8 } };
  if (SoppalusikkaConfig.usesinglearea && osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
     osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
  else {
     tArea Areas[] = { { x0, y0, x6 - 1, y7 - 1, 4 } };
     if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
        osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
     }
  // clear all
  osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
  // draw titlebar
  osd->DrawRectangle(x0, y0, x4 - 1, y2 - 1, Theme.Color(clrMenuTitleBg));
  osd->DrawText(x1, y0, Title, Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg), font, x5 - x1, y2 - y0);
  // draw rounded left corner of titlebar
  osd->DrawEllipse(x0, y0, x1 - 1, y1 - 1, clrTransparent, -2);
  // draw rounded right corner of titlebar
  osd->DrawEllipse(x4, y0, x5 - 1, y1 - 1, clrTransparent, -1);
  // draw tracks area
  osd->DrawRectangle(x0, y2, x5 - 1, y5 - 1, Theme.Color(clrBackground));
  // draw bottombar
  osd->DrawRectangle(x0, y5, x5 - 1, y7 - 1, Theme.Color(clrMenuTitleBg));
  // draw rounded left corner of titlebar
  osd->DrawEllipse(x0, y6, x1 - 1, y7 - 1, clrTransparent, -3);
  // draw rounded right corner of titlebar
  osd->DrawEllipse(x4, y6, x5 - 1, y7 - 1, clrTransparent, -4);
  // fill up audio tracks
  SetAudioChannel(cDevice::PrimaryDevice()->GetAudioChannel());
  for (int i = 0; i < NumTracks; i++)
      SetItem(Tracks[i], i, false);
}

cSkinSoppalusikkaDisplayTracks::~cSkinSoppalusikkaDisplayTracks()
{
  delete osd;
}

void cSkinSoppalusikkaDisplayTracks::SetItem(const char *Text, int Index, bool Current)
{
  const cFont *font = cFont::GetFont(fontOsd);
  int y = y3 + Index * lineHeight;
  tColor ColorFg, ColorBg;
  if (Current) {
     ColorFg = Theme.Color(clrMenuItemCurrentFg);
     ColorBg = Theme.Color(clrMenuItemCurrentBg);
     currentIndex = Index;
     }
  else {
     ColorFg = Theme.Color(clrMenuItemSelectable);
     ColorBg = Theme.Color(clrBackground);
     }
  // draw track id
  osd->DrawText(x1, y, Text, ColorFg, ColorBg, font, x4 - x1, lineHeight);
  // draw rounded left corner
  osd->DrawEllipse(x0 + SmallGap, y, x1 - 1, y + lineHeight - 1, ColorBg, 7);
  // draw rounded right corner
  osd->DrawEllipse(x4, y, x5 - SmallGap - 1, y + lineHeight - 1, ColorBg, 5);
}

void cSkinSoppalusikkaDisplayTracks::SetAudioChannel(int AudioChannel)
{
  const cFont *font = cFont::GetFont(fontSml);
  switch (AudioChannel) {
    case 0:
         /* stereo */
         osd->DrawBitmap(x3, y0 + (y2 - bmAudio.Height()) / 2, bmAudio, Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg));
         osd->DrawText(x1, y5, tr("Stereo"), Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg), font, x4 - x1, y7 - y5, taCenter);
         break;
    case 1:
         /* mono left */
         osd->DrawBitmap(x3, y0 + (y2 - bmAudio.Height()) / 2, bmAudioLeft, Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg));
         osd->DrawText(x1, y5, tr("Left"), Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg), font, x4 - x1, y7 - y5, taCenter);
         break;
    case 2:
         /* mono right */
         osd->DrawBitmap(x3, y0 + (y2 - bmAudio.Height()) / 2, bmAudioRight, Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg));
         osd->DrawText(x1, y5, tr("Right"), Theme.Color(clrMenuTitleFg), Theme.Color(clrMenuTitleBg), font, x4 - x1, y7 - y5, taCenter);
         break;
    }
}

void cSkinSoppalusikkaDisplayTracks::SetTrack(int Index, const char * const *Tracks)
{
  if (currentIndex >= 0)
     SetItem(Tracks[currentIndex], currentIndex, false);
  SetItem(Tracks[Index], Index, true);
}

void cSkinSoppalusikkaDisplayTracks::Flush(void)
{
  osd->Flush();
}

// --- cSkinSoppalusikkaDisplayMessage --------------------------------------------

class cSkinSoppalusikkaDisplayMessage : public cSkinDisplayMessage {
private:
  cOsd *osd;
  int x0, x1, x2, x3;
  int y0, y1;
  int lineHeight;
public:
  cSkinSoppalusikkaDisplayMessage();
  virtual ~cSkinSoppalusikkaDisplayMessage();
  virtual void SetMessage(eMessageType Type, const char *Text);
  virtual void Flush(void);
  };

cSkinSoppalusikkaDisplayMessage::cSkinSoppalusikkaDisplayMessage()
{
  lineHeight = cFont::GetFont(fontOsd)->Height();
  x0 = 0;
  x1 = x0 + Roundness;
  x3 = cOsd::OsdWidth();
  x2 = x3 - Roundness;
  y0 = 0;
  y1 = y0 + lineHeight;
  // create osd
  osd = cOsdProvider::NewOsd(cOsd::OsdLeft(), cOsd::OsdTop() + cOsd::OsdHeight() - y1);
  // try to use single 8bpp area
  tArea Areas[] = { { x0, y0, x3 - 1, y1 - 1, 8 } };
  if (SoppalusikkaConfig.usesinglearea && osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
     osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
  else {
     tArea Areas[] = { { x0, y0, x3 - 1, y1 - 1, 2 } };
     if (osd->CanHandleAreas(Areas, sizeof(Areas) / sizeof(tArea)) == oeOk)
        osd->SetAreas(Areas, sizeof(Areas) / sizeof(tArea));
     }
  // clear all
  osd->DrawRectangle(0, 0, osd->Width(), osd->Height(), clrTransparent);
}

cSkinSoppalusikkaDisplayMessage::~cSkinSoppalusikkaDisplayMessage()
{
  delete osd;
}

void cSkinSoppalusikkaDisplayMessage::SetMessage(eMessageType Type, const char *Text)
{
  // draw rounded left corner
  osd->DrawEllipse(x0, y0, x1 - 1, y1 - 1, Theme.Color(clrMessageStatusBg + 2 * Type), 7);
  // draw centered message text
  osd->DrawText(x1, y0, Text, Theme.Color(clrMessageStatusFg + 2 * Type), Theme.Color(clrMessageStatusBg + 2 * Type), cFont::GetFont(fontOsd), x2 - x1, y1 - y0, taCenter);
  // draw rounded right corner
  osd->DrawEllipse(x2, y0, x3 - 1, y1 - 1, Theme.Color(clrMessageStatusBg + 2 * Type), 5);
}

void cSkinSoppalusikkaDisplayMessage::Flush(void)
{
  osd->Flush();
}

// --- cSkinSoppalusikka ----------------------------------------------------------

cSkinSoppalusikka::cSkinSoppalusikka()
:cSkin("soppalusikka", &::Theme)
{
}

const char *cSkinSoppalusikka::Description(void)
{
  return tr("Soppalusikka");
}

cSkinDisplayChannel *cSkinSoppalusikka::DisplayChannel(bool WithInfo)
{
  return new cSkinSoppalusikkaDisplayChannel(WithInfo);
}

cSkinDisplayMenu *cSkinSoppalusikka::DisplayMenu(void)
{
  return new cSkinSoppalusikkaDisplayMenu;
}

cSkinDisplayReplay *cSkinSoppalusikka::DisplayReplay(bool ModeOnly)
{
  return new cSkinSoppalusikkaDisplayReplay(ModeOnly);
}

cSkinDisplayVolume *cSkinSoppalusikka::DisplayVolume(void)
{
  return new cSkinSoppalusikkaDisplayVolume;
}

cSkinDisplayTracks *cSkinSoppalusikka::DisplayTracks(const char *Title, int NumTracks, const char * const *Tracks)
{
  return new cSkinSoppalusikkaDisplayTracks(Title, NumTracks, Tracks);
}

cSkinDisplayMessage *cSkinSoppalusikka::DisplayMessage(void)
{
  return new cSkinSoppalusikkaDisplayMessage;
}
