vdr/menu.c
Klaus Schmidinger a26aae3ce8 Version 2.3.1
VDR developer version 2.3.1 is now available at

       ftp://ftp.tvdr.de/vdr/Developer/vdr-2.3.1.tar.bz2

A 'diff' against the previous version is available at

       ftp://ftp.tvdr.de/vdr/Developer/vdr-2.2.0-2.3.1.diff

MD5 checksums:

391c2ed60e2f7d24563fe3ed5854bc4f  vdr-2.3.1.tar.bz2
983fd4bad7d19cd98301d54173107129  vdr-2.2.0-2.3.1.diff

WARNING:
========

This is a *developer* version. Even though *I* use it in my productive
environment, I strongly recommend that you only use it under controlled
conditions and for testing and debugging.

*** PLEASE BE VERY CAREFUL WHEN USING THIS DEVELOPER VERSION, ESPECIALLY
*** IF YOU ENABLE THE NEW SVDRP PEERING! KEEP BACKUPS OF ALL YOUR TIMERS
*** AND OBSERVE VERY CLOSELY WHETHER EVERYTHING WORKS AS EXPECTED. THIS
*** VERSION INTRODUCES SOME MAJOR CHANGES IN HANDLING GLOBAL LISTS AND
*** LOCKING, SO ANYTHING CAN HAPPEN! YOU HAVE BEEN WARNED!

The main focus of this developer version is on the new locking mechanism
for global lists, and the ability to handle remote timers.
Any plugins that access the global lists of timers, channels, schedules
or recordings, will need to be adjusted (see below for details). Please
do initial tests with plain vanilla VDR and just the output plugin you
need.

Known bugs/problems:

- After deleting the last recording in a sub folder, the cursor may not
   be positioned correctly.
- Instant recordings and pausing live video don't (yet) use the default
   SVDRP host for recording.

From the HISTORY file:
 - The new function cOsd::MaxPixmapSize() can be called to determine the maximum size
  a cPixmap may have on the current OSD. The 'osddemo' example has been modified
  accordingly. Plugin authors may want to use this function in case they use pixmaps
  that are larger than the full OSD size. The default implementation sets this limit
  to 2048x2048 pixel.
- The Setup/CAM menu now displays which device an individual CAM is currently
  assigned to (suggested by Frank Neumann).
- Added detection of 24fps (thanks to Thomas Reufer).
- Added a note about the VDR User Counter and VDR's facebook page to the README file.
- The dvbhddevice plugin is no longer part of the VDR source archive.
  You can get the latest version of this plugin from the author's repository at
  https://bitbucket.org/powARman/dvbhddevice.
- The dvbsddevice and rcu plugins are no longer part of the VDR source archive.
  You can get the latest versions of these plugins from ftp://ftp.tvdr.de/vdr/Plugins.
- Added a section about Output Devices to the INSTALL file.
- Fixed setting the source value of newly created channels, in case the NIT is
  received from a different, but very close satellite position (reported by Daniel
  Ribeiro). The code for handling different NITs has been removed from nit.c, because
  according to the DVB standard table id 0x40 carries only the NIT of the actual
  network.
- Added some comment to cPixmap about the relation between OSD, ViewPort and DrawPort
  (suggested by Thomas Reufer).
- Improved syncing on sections when parsing the NIT and SDT.
- Fixed scaling subtitles (their areas could sometimes extend outside the actual OSD).
- Reduced the priority of the "video directory scanner" thread (suggested by Thomas
  Reufer) and checking cIoThrottle::Engaged() when it is running.
- The script that gets called for recordings is now also called right before a
  recording is edited, with the first parameter being "editing" (suggested by
  Dieter Ferdinand).
- The new setup option "OSD/Default sort mode for recordings" can be used to define
  how recordings shall be sorted by default (either by time or by name, with "by time"
  being the default). If a particular sort mode has been selected for a folder by
  pressing '0', the default no longer applies to that folder. Repeating timers no
  longer write a ".sort" file into a recordings folder to have the recordings sorted
  by time.
- The command line option -D now accepts the value '-' (as in -D-), which prevents
  VDR from using any DVB devices (suggested by Dietmar Spingler).
- The -V and -h options now list the plugins in alphabetical order (suggested by
  Dietmar Spingler).
- Fixed a compiler warning in font.c.
- Commented out the line
  #define DEPRECATED_VIDEOSYSTEM
  in device.h. If a plugin doesn't compile with this version of VDR, you can uncomment
  this line as a quick workaround. In the long run the plugin will need to be adapted.
- The function cOsd::GetBitmap() is now 'protected'. If a plugin doesn't compile with
  this version of VDR, you can uncomment the line
  //#define DEPRECATED_GETBITMAP
  in osd.h as a quick workaround. In the long run the plugin will need to be adapted.
- The -u option now also accepts a numerical user id (suggested by Derek Kelly).
- The SVDRP port now accepts multiple concurrent connections. You can now keep an
  SVDRP connection open as long as you wish, without preventing others from
  connecting. Note, though, that SVDRP connections still get closed automatically
  if there has been no activity for 300 seconds (configurable via
  "Setup/Miscellaneous/SVDRP timeout (s)").
- The SVDRP log messages have been unified and now always contain the IP and port
  number of the remote host.
- SVDRP connections are now handled in a separate "SVDRP server handler" thread,
  which makes them more responsive. Note that there is only one thread that handles
  all concurrent SVDRP connections. That way each SVDRP command is guaranteed to be
  processed separately, without interfering with any other SVDRP commands that might
  be issued at the same time. Plugins that implement SVDRP commands may need to take
  care of proper locking if the commands access global data.
- VDR now sends out a broadcast to port 6419/udp, which was assigned to 'svdrp-disc'
  by the IANA. VDRs listening on that port will automatically initiate an SVDRP
  connection to the broadcasting VDR, and in turn send out a broadcast to make
  other VDRs connect to them. That way all VDRs within the local network will
  have permanent "peer-to-peer" SVDRP connections between each other. The
  configuration in the svdrphosts.conf file is taken into account when considering
  whether or not to respond to an SVDRP discover broadcast.
- The new SVDRP command PING is used by automatically established peer-to-peer
  connections to keep them alive.
- The new function GetSVDRPServerNames() can be used to get a list of all VDRs
  this VDR is connected to via SVDRP.
- The new function ExecSVDRPCommand() can be used to execute an SVDRP command on
  one of the servers this VDR is connected to, and retrieve the result.
  The helper functions SVDRPCode() and SVDRPValue() can be used to easily access
  the codes and values returned by ExecSVDRPCommand().
- The cTimer class now has a new member named 'remote', which holds the name of the
  remote server this timer will record on. If this is NULL, it is a local timer.
- Timers from other VDRs that are connected to this VDR via SVDRP are now
  automatically fetched and stored in the global Timers list. In order for this
  to work, all of the channels used by timers on the remote VDR must also be
  defined on the local VDR (however, not necessarily in the same sequence).
  Automatic channel syncing will be implemented later.
- The main menu of the LCARS skin now displays a small rectangle on the left side
  of a timer if this is a remote timer. The color of that rectangle changes if
  the timer is currently recording on the remote VDR.
- Accessing the global Timers list now has to be protected by proper locking,
  because SVDRP commands are now executed in a separate thread.
  The introduction of this locking mechanism required the following changes:
  + The new classes cStateLock and cStateKey are used to implement locking
    with quick detection of state changes.
  + cConfig::cConfig() now has a parameter that indicates whether this list
    requires locking.
  + The global lists of Timers, Channels, Schedules and Recordings are no longer
    static variables. They are now pointers that need to be retrieved through
    a call to cTimers::GetTimersRead/Write(), cChannels::GetChannelsRead/Write(),
    cSchedules::GetSchedulesRead/Write() and cRecordings::GetRecordingsRead/Write(),
    respectively.
  + References from/to link channels are now removed in cChannels::Del() rather
    than cChannel::~cChannel(), to make sure the caller holds a proper lock.
  + cChannel::HasTimer() has been removed. This information is now retrieved
    via cSchedule::HasTimer().
  + Several member functions of cChannel, cTimer, cMarks and cRecording have
    been made 'const', and some of them are now available as both 'const' and
    'non-const' versions.
  + The cChannel::Set...() functions are now 'bool' and return true if they have
    actually changed any of the channels's members.
  + cChannels::SetModified() has been renamed to cChannels::SetModifiedByUser().
  + cChannels::Modified() has been renamed to cChannels::ModifiedByUser(), and
    now has a 'State' parameter that allows the caller to see whether a channel
    has been modified since the last call to this function with the same State
    variable.
  + The macros CHANNELSMOD_NONE/_AUTO/_USER have been removed.
  + cMarks now requires locking via cStateKey.
  + cSortedTimers now requires a pointer to the list of timers.
  + cEvent::HasTimer() no longer scans the list of timers to check whether an event
    is referenced by a timer, but rather keeps score of how many timers reference
    it. This was necessary in order to avoid having to lock the list of timers from
    within a cEvent.
  + The new class cListGarbageCollector is used to temporary store any objects deleted
    from cLists that require locking. This allows pointers to such objects to be
    dereferenced even if the objects are no longer part of the list.
  + cListBase::Contains() can be used to check whether a particular object is still
    contained in that list.
  + Outdated events are no longer "phased out", but rather deleted right away and thus
    taken care of by the new "garbage collector" of the list.
  + Deleted cRecording objects are no longer kept in a list of "vanished" recordings,
    but are rather taken care of by the new "garbage collector" of the list.
  + cSchedules::ClearAll() has been removed. The functionality is now implemented
    directly in cSVDRPServer::CmdCLRE().
  + tEventID has been changed to u_int16_t in order to make room for the new member
    numTimers in cEvent.
  + cSchedule now has a member Modified(), which can be used with a State variable
    to quickly determine whether this schedule has been modified since the last call
    to this function with the same State variable.
  + cSchedulesLock has been removed. Locking the list of schedules is now done via
    the cList's new locking mechanism.
  + The 'OnlyRunningStatus' parameters in cEpgHandler::BeginSegmentTransfer() and
    cEpgHandler::EndSegmentTransfer() are now obsolete. They are still present in
    the interface for backward compatibility, but may be removed in a future version.
    Their value is always 'false'.
  + The constant tcMod is no longer used in cStatus::TimerChange(). The definition is
    still there for backward compatibility.
  Plugins that access the global lists of Timers, Channels, Recordings or Schedules
  will need to be adapted as follows:
  + Instead of directly accessing the global variables Timers, Channels or Recordings,
    they need to set up a cStateKey variable and call the proper getter function,
    as in
      cStateKey StateKey;
      if (const cTimers *Timers = cTimers::GetTimersRead(StateKey)) {
         // access the timers
         StateKey.Remove();
         }
    and
      cStateKey StateKey;
      if (cTimers *Timers = cTimers::GetTimersWrite(StateKey)) {
         // access the timers
         StateKey.Remove();
         }
    See timers.h, thread.h and tools.h for details on this new locking mechanism.
  + There are convenience macros for easily accessing these lists without having
    to explicitly set up a cStateKey and calling its Remove() function. These macros
    have the form LOCK_*_READ/WRITE (with '*' being TIMERS, CHANNELS, SCHEDULES or
    RECORDINGS). Simply put such a macro before the point where you need to access
    the respective list, and there will be a pointer named Timers, Channels, Schedules
    or Recordings, respectively, which is valid until the end of the current block.
  + If a plugin needs to access several of the global lists in parallel, locking must
    always be done in the sequence Timers, Channels, Recordings, Schedules. This is
    necessary to make sure that different threads that need to lock several lists at
    the same time don't end up in a deadlock.
  + Some pointer variables may need to be made 'const'. The compiler will tell you
    about these.
- cSectionSyncer has been improved to better handle missed sections.
- Added a missing initialization of 'seen' in cChannel's copy constructor.
- Background modifications of channels, timers and events are now displayed immediately
  in the corresponding menus.
- cEIT now checks the version of the tables before doing any processing, which saves
  a lot of locking and processing.
- If a timer is newly created with the Red button in the Schedule menu, and the timer
  is presented to the user in the "Edit timer" menu because it will start immediately,
  it now *must* be confirmed with "Ok" to set the timer. Otherwise the timer will not
  be created.
- Recordings and deleted recordings are now scanned in a single thread.
- The new SVDRP command POLL is used by automatically established peer-to-peer
  connections to trigger fetching remote timers.
- You can now set DumpSVDRPDataTransfer in svdrp.c to true to have all SVDRP
  communication printed to the console for debugging.
- Added a missing 'const' to cReceiver::Receive(), to protect the given Data from
  being modified.
- The SVDRP commands that deal with timers (DELT, LSTT, MODT, NEWT, NEXT and UPDT)
  as well as any log messages that refer to timers, now use a unique id for each
  timer, which remains valid as long as this instance of VDR is running. This means
  that timers are no longer continuously numbered from 1 to N in LSTT. There may be
  gaps in the sequence, in case timers have been deleted.
- The Timers menu now displays the name of the remote VDR in front of the timer's
  file name, if this is a remote timer.
- The new options "Setup/Miscellaneous/SVDRP peering", ".../SVDRP host name" and
  ".../SVDRP default host" can be used to configure automatic peering between VDRs
  in the same network. Peering is disabled by default and can be enabled by setting
  "SVDRP peering" to "yes".
- The function cTimer::ToText() no longer returns a newline character at the end of
  the string. The newline is now added by the caller as necessary. This was changed
  because cTimer::ToText() is now also needed in a context where the terminating
  newline can't be used. Consequently, cChannel::ToText() and cMark::ToText() have
  been modified accordingly.
- All timer related response strings from SVDRP commands now use the channel ID
  instead of channel numbers.
- The "Edit timer" menu now has a new parameter "Record on", which can be used to
  select the VDR on which this timer shall record. Timers can be freely moved
  between connected VDRs by simply selecting the desired machine in this field.
- The SVDRP command DELT no longer checks whether the timer that shall be deleted
  is currently recording.
- The character 0x0D is now stripped from EPG texts (reported by Janne Pänkälä).
- The EPG scanner no longer moves the dish if there is a positioner.
- The 'newplugin' script now creates the 'po' subdirectory for translations (thanks
  to Thomas Reufer).
- Skins can now implement cSkinDisplayMenu::MenuOrientation() to display horizontal
  menus (thanks to Stefan Braun).
- Fixed a possible stack overflow in cListBase::Sort() (thanks to Oliver Endriss).
- Changed the description of the --chartab option in the INSTALL file to refer to
  "DVB SI table strings" instead of "EPG data".
- The width and height of the OSD are now limited to the actual maximum dimensions
  of the output device, taking into account the top and left offset.
- The new setup option "Recording/Record key handling" can be used to define
  what happens if the Record key on the remote control is pressed during
  live tv (suggested by Dietmar Spingler).
- Empty adaptation field TS packets are now skipped when recording (thanks to
  Christopher Reimer, based on the "AFFcleaner" by Stefan Pöschel).
2015-09-18 00:04:12 +02:00

5972 lines
194 KiB
C

/*
* 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 4.12 2015/09/14 13:22:49 kls Exp $
*/
#include "menu.h"
#include <ctype.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 "sourceparams.h"
#include "sources.h"
#include "status.h"
#include "svdrp.h"
#include "themes.h"
#include "timers.h"
#include "transfer.h"
#include "videodir.h"
#define MAXWAIT4EPGINFO 3 // seconds
#define MODETIMEOUT 3 // seconds
#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 DEFERTIMER 60 // seconds by which a timer is deferred in case of problems
#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 MAXCHNAMWIDTH 16 // maximum number of characters of channels' short names shown in schedules menus
#define CHNUMWIDTH (numdigits(cChannels::MaxNumber()) + 1)
#define CHNAMWIDTH (min(MAXCHNAMWIDTH, cChannels::MaxShortChannelNameLength() + 1))
// --- 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) {
bool IsRepeat = Key & k_Repeat;
Key = NORMALKEY(Key);
if (Key == kLeft) { // TODO might want to increase the delta if repeated quickly?
if (source) {
if (source->Prev())
source = (cSource *)source->Prev();
else if (!IsRepeat)
source = Sources.Last();
*value = source->Code();
}
}
else if (Key == kRight) {
if (source) {
if (source->Next())
source = (cSource *)source->Next();
else if (!IsRepeat)
source = Sources.First();
}
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;
}
// --- cMenuEditChannel ------------------------------------------------------
class cMenuEditChannel : public cOsdMenu {
private:
cStateKey *channelsStateKey;
cChannel *channel;
cChannel data;
cSourceParam *sourceParam;
char name[256];
void Setup(void);
public:
cMenuEditChannel(cStateKey *ChannelsStateKey, cChannel *Channel, bool New = false);
cChannel *Channel(void) { return channel; }
virtual eOSState ProcessKey(eKeys Key);
};
cMenuEditChannel::cMenuEditChannel(cStateKey *ChannelsStateKey, cChannel *Channel, bool New)
:cOsdMenu(tr("Edit channel"), 16)
{
SetMenuCategory(mcChannelEdit);
channelsStateKey = ChannelsStateKey;
channel = Channel;
sourceParam = NULL;
*name = 0;
if (channel) {
data = *channel;
strn0cpy(name, data.name, sizeof(name));
if (New) {
channel = NULL;
// clear non-editable members:
data.nid = 0;
data.tid = 0;
data.rid = 0;
*data.shortName = 0;
*data.provider = 0;
*data.portalName = 0;
}
}
Setup();
}
void cMenuEditChannel::Setup(void)
{
int current = Current();
Clear();
// Parameters for all types of sources:
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));
Add(new cMenuEditIntItem( tr("Nid"), &data.nid, 0));
Add(new cMenuEditIntItem( tr("Tid"), &data.tid, 0));
/* XXX not yet used
Add(new cMenuEditIntItem( tr("Rid"), &data.rid, 0));
XXX*/
// Parameters for specific types of sources:
sourceParam = SourceParams.Get(**cSource::ToString(data.source));
if (sourceParam) {
sourceParam->SetData(&data);
cOsdItem *Item;
while ((Item = sourceParam->GetOsdItem()) != NULL)
Add(Item);
}
SetCurrent(Get(current));
Display();
}
eOSState cMenuEditChannel::ProcessKey(eKeys Key)
{
int oldSource = data.source;
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
if (Key == kOk) {
cChannels *Channels =cChannels::GetChannelsWrite(*channelsStateKey);
bool Modified = false;
if (sourceParam)
sourceParam->GetData(&data);
if (Channels->HasUniqueChannelID(&data, channel)) {
data.name = strcpyrealloc(data.name, name);
if (channel) {
*channel = data;
isyslog("edited channel %d %s", channel->Number(), *channel->ToText());
state = osBack;
}
else {
channel = new cChannel;
*channel = data;
Channels->Add(channel);
Channels->ReNumber();
isyslog("added channel %d %s", channel->Number(), *channel->ToText());
state = osUser1;
}
Channels->SetModifiedByUser();
Modified = true;
}
else {
Skins.Message(mtError, tr("Channel settings are not unique!"));
state = osContinue;
}
channelsStateKey->Remove(Modified);
}
}
if (Key != kNone && (data.source & cSource::st_Mask) != (oldSource & cSource::st_Mask)) {
LOCK_CHANNELS_WRITE;
if (sourceParam)
sourceParam->GetData(&data);
Setup();
}
return state;
}
// --- cMenuChannelItem ------------------------------------------------------
class cMenuChannelItem : public cOsdItem {
public:
enum eChannelSortMode { csmNumber, csmName, csmProvider };
private:
static eChannelSortMode sortMode;
const cChannel *channel;
public:
cMenuChannelItem(const 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);
const cChannel *Channel(void) { return channel; }
virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable);
};
cMenuChannelItem::eChannelSortMode cMenuChannelItem::sortMode = csmNumber;
cMenuChannelItem::cMenuChannelItem(const 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());
else
buffer = cString::sprintf("%d\t%s", channel->Number(), channel->Name());
}
else
buffer = cString::sprintf("---\t%s ----------------------------------------------------------------", channel->Name());
SetText(buffer);
}
void cMenuChannelItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
{
if (!DisplayMenu->SetItemChannel(channel, Index, Current, Selectable, sortMode == csmProvider))
DisplayMenu->SetItem(Text(), Index, Current, Selectable);
}
// --- cMenuChannels ---------------------------------------------------------
#define CHANNELNUMBERTIMEOUT 1000 //ms
class cMenuChannels : public cOsdMenu {
private:
cStateKey channelsStateKey;
int number;
cTimeMs numberTimer;
void Set(bool Force = false);
cChannel *GetChannel(int Index);
void Propagate(cChannels *Channels);
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);
};
cMenuChannels::cMenuChannels(void)
:cOsdMenu(tr("Channels"), CHNUMWIDTH)
{
SetMenuCategory(mcChannel);
number = 0;
Set();
}
cMenuChannels::~cMenuChannels()
{
}
void cMenuChannels::Set(bool Force)
{
if (Force)
channelsStateKey.Reset();
if (const cChannels *Channels = cChannels::GetChannelsRead(channelsStateKey)) {
const cChannel *CurrentChannel = GetChannel(Current());
if (!CurrentChannel)
CurrentChannel = Channels->GetByNumber(cDevice::CurrentChannel());
cMenuChannelItem *CurrentItem = NULL;
Clear();
for (const 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;
}
}
SetMenuSortMode(cMenuChannelItem::SortMode() == cMenuChannelItem::csmName ? msmName :
cMenuChannelItem::SortMode() == cMenuChannelItem::csmProvider ? msmProvider :
msmNumber);
if (cMenuChannelItem::SortMode() != cMenuChannelItem::csmNumber)
Sort();
SetCurrent(CurrentItem);
SetHelp(tr("Button$Edit"), tr("Button$New"), tr("Button$Delete"), tr("Button$Mark"));
Display();
channelsStateKey.Remove();
}
}
cChannel *cMenuChannels::GetChannel(int Index)
{
cMenuChannelItem *p = (cMenuChannelItem *)Get(Index);
return p ? (cChannel *)p->Channel() : NULL;
}
void cMenuChannels::Propagate(cChannels *Channels)
{
Channels->ReNumber();
for (cMenuChannelItem *ci = (cMenuChannelItem *)First(); ci; ci = (cMenuChannelItem *)ci->Next())
ci->Set();
Display();
Channels->SetModifiedByUser();
}
eOSState cMenuChannels::Number(eKeys Key)
{
if (HasSubMenu())
return osContinue;
if (numberTimer.TimedOut())
number = 0;
if (!number && Key == k0) {
cMenuChannelItem::IncSortMode();
Set(true);
}
else {
LOCK_CHANNELS_READ;
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;
LOCK_CHANNELS_READ;
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;
LOCK_CHANNELS_READ;
cChannel *ch = GetChannel(Current());
if (ch)
return AddSubMenu(new cMenuEditChannel(&channelsStateKey, ch));
return osContinue;
}
eOSState cMenuChannels::New(void)
{
if (HasSubMenu())
return osContinue;
LOCK_CHANNELS_READ;
return AddSubMenu(new cMenuEditChannel(&channelsStateKey, GetChannel(Current()), true));
}
eOSState cMenuChannels::Delete(void)
{
if (!HasSubMenu() && Count() > 0) {
LOCK_TIMERS_READ; // must lock timers before channels!
cChannels *Channels = cChannels::GetChannelsWrite(channelsStateKey);
int Index = Current();
cChannel *Channel = GetChannel(Current());
if (!Channels->Contains(Channel)) {
channelsStateKey.Remove(false);
channelsStateKey.Reset(); // makes sure the menu is refreshed
return osContinue;
}
bool Deleted = false;
int CurrentChannelNr = cDevice::CurrentChannel();
cChannel *CurrentChannel = Channels->GetByNumber(CurrentChannelNr);
int DeletedChannel = Channel->Number();
// Check if there is a timer using this channel:
if (Timers->UsesChannel(Channel)) {
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);
Channels->SetModifiedByUser();
isyslog("channel %d deleted", DeletedChannel);
Deleted = true;
if (CurrentChannel && CurrentChannel->Number() != CurrentChannelNr) {
if (!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring())
Channels->SwitchTo(CurrentChannel->Number());
else
cDevice::SetCurrentChannel(CurrentChannel);
}
}
channelsStateKey.Remove(Deleted);
}
return osContinue;
}
void cMenuChannels::Move(int From, int To)
{
if (cChannels *Channels = cChannels::GetChannelsWrite(channelsStateKey)) {
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);
Channels->SetModifiedByUser();
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);
}
}
channelsStateKey.Remove();
}
}
eOSState cMenuChannels::ProcessKey(eKeys Key)
{
if (!HasSubMenu())
Set(); // react on any changes to the channels list
eOSState state = cOsdMenu::ProcessKey(Key);
switch (state) {
case osUser1: {
if (cMenuEditChannel *MenuEditChannel = dynamic_cast<cMenuEditChannel *>(SubMenu())) {
if (cChannel *Channel = MenuEditChannel->Channel()) {
LOCK_CHANNELS_READ;
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)
{
SetMenuCategory(mcText);
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 (int(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;
}
// --- cMenuFolderItem -------------------------------------------------------
class cMenuFolderItem : public cOsdItem {
private:
cNestedItem *folder;
public:
cMenuFolderItem(cNestedItem *Folder);
cNestedItem *Folder(void) { return folder; }
};
cMenuFolderItem::cMenuFolderItem(cNestedItem *Folder)
:cOsdItem(Folder->Text())
{
folder = Folder;
if (folder->SubItems())
SetText(cString::sprintf("%s...", folder->Text()));
}
// --- cMenuEditFolder -------------------------------------------------------
class cMenuEditFolder : public cOsdMenu {
private:
cList<cNestedItem> *list;
cNestedItem *folder;
char name[PATH_MAX];
int subFolder;
eOSState Confirm(void);
public:
cMenuEditFolder(const char *Dir, cList<cNestedItem> *List, cNestedItem *Folder = NULL);
cString GetFolder(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuEditFolder::cMenuEditFolder(const char *Dir, cList<cNestedItem> *List, cNestedItem *Folder)
:cOsdMenu(Folder ? tr("Edit folder") : tr("New folder"), 12)
{
SetMenuCategory(mcFolder);
list = List;
folder = Folder;
if (folder) {
strn0cpy(name, folder->Text(), sizeof(name));
subFolder = folder->SubItems() != NULL;
}
else {
*name = 0;
subFolder = 0;
cRemote::Put(kRight, true); // go right into string editing mode
}
if (!isempty(Dir)) {
cOsdItem *DirItem = new cOsdItem(Dir);
DirItem->SetSelectable(false);
Add(DirItem);
}
Add(new cMenuEditStrItem( tr("Name"), name, sizeof(name)));
Add(new cMenuEditBoolItem(tr("Sub folder"), &subFolder));
}
cString cMenuEditFolder::GetFolder(void)
{
return folder ? folder->Text() : "";
}
eOSState cMenuEditFolder::Confirm(void)
{
if (!folder || strcmp(folder->Text(), name) != 0) {
// each name may occur only once in a folder list
for (cNestedItem *Folder = list->First(); Folder; Folder = list->Next(Folder)) {
if (strcmp(Folder->Text(), name) == 0) {
Skins.Message(mtError, tr("Folder name already exists!"));
return osContinue;
}
}
char *p = strpbrk(name, "\\{}#~"); // FOLDERDELIMCHAR
if (p) {
Skins.Message(mtError, cString::sprintf(tr("Folder name must not contain '%c'!"), *p));
return osContinue;
}
}
if (folder) {
folder->SetText(name);
folder->SetSubItems(subFolder);
}
else
list->Add(folder = new cNestedItem(name, subFolder));
return osEnd;
}
eOSState cMenuEditFolder::ProcessKey(eKeys Key)
{
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
switch (Key) {
case kOk: return Confirm();
case kRed:
case kGreen:
case kYellow:
case kBlue: return osContinue;
default: break;
}
}
return state;
}
// --- cMenuFolder -----------------------------------------------------------
cMenuFolder::cMenuFolder(const char *Title, cNestedItemList *NestedItemList, const char *Path)
:cOsdMenu(Title)
{
SetMenuCategory(mcFolder);
list = nestedItemList = NestedItemList;
firstFolder = NULL;
editing = false;
helpKeys = -1;
Set();
DescendPath(Path);
Display();
SetHelpKeys();
}
cMenuFolder::cMenuFolder(const char *Title, cList<cNestedItem> *List, cNestedItemList *NestedItemList, const char *Dir, const char *Path)
:cOsdMenu(Title)
{
SetMenuCategory(mcFolder);
list = List;
nestedItemList = NestedItemList;
dir = Dir;
firstFolder = NULL;
editing = false;
helpKeys = -1;
Set();
DescendPath(Path);
Display();
SetHelpKeys();
}
void cMenuFolder::SetHelpKeys(void)
{
if (HasSubMenu())
return;
int NewHelpKeys = 0;
if (firstFolder) {
if (cMenuFolderItem *Folder = (cMenuFolderItem *)Get(Current())) {
if (Folder->Folder()->SubItems())
NewHelpKeys = 1;
}
}
if (NewHelpKeys != helpKeys) {
helpKeys = NewHelpKeys;
SetHelp(NewHelpKeys > 0 ? tr("Button$Open") : NULL, tr("Button$New"), firstFolder ? tr("Button$Delete") : NULL, firstFolder ? tr("Button$Edit") : NULL);
}
}
#define FOLDERDELIMCHARSUBST 0x01
static void AddRecordingFolders(const cRecordings *Recordings, cList<cNestedItem> *List, char *Path)
{
if (Path) {
char *p = strchr(Path, FOLDERDELIMCHARSUBST);
if (p)
*p++ = 0;
cNestedItem *Folder;
for (Folder = List->First(); Folder; Folder = List->Next(Folder)) {
if (strcmp(Path, Folder->Text()) == 0)
break;
}
if (!Folder)
List->Add(Folder = new cNestedItem(Path));
if (p) {
Folder->SetSubItems(true);
AddRecordingFolders(Recordings, Folder->SubItems(), p);
}
}
else {
cStringList Dirs;
for (const cRecording *Recording = Recordings->First(); Recording; Recording = Recordings->Next(Recording)) {
cString Folder = Recording->Folder();
strreplace((char *)*Folder, FOLDERDELIMCHAR, FOLDERDELIMCHARSUBST); // makes sure parent folders come before subfolders
if (Dirs.Find(Folder) < 0)
Dirs.Append(strdup(Folder));
}
Dirs.Sort();
for (int i = 0; i < Dirs.Size(); i++) {
if (char *s = Dirs[i])
AddRecordingFolders(Recordings, &Folders, s);
}
}
}
void cMenuFolder::Set(const char *CurrentFolder)
{
static cStateKey RecordingsStateKey;
if (list == &Folders) {
if (const cRecordings *Recordings = cRecordings::GetRecordingsRead(RecordingsStateKey)) {
AddRecordingFolders(Recordings, &Folders, NULL);
RecordingsStateKey.Remove();
}
}
firstFolder = NULL;
Clear();
if (!isempty(dir)) {
cOsdItem *DirItem = new cOsdItem(dir);
DirItem->SetSelectable(false);
Add(DirItem);
}
list->Sort();
for (cNestedItem *Folder = list->First(); Folder; Folder = list->Next(Folder)) {
cOsdItem *FolderItem = new cMenuFolderItem(Folder);
Add(FolderItem, CurrentFolder ? strcmp(Folder->Text(), CurrentFolder) == 0 : false);
if (!firstFolder)
firstFolder = FolderItem;
}
}
void cMenuFolder::DescendPath(const char *Path)
{
if (Path) {
const char *p = strchr(Path, FOLDERDELIMCHAR);
if (p) {
for (cMenuFolderItem *Folder = (cMenuFolderItem *)firstFolder; Folder; Folder = (cMenuFolderItem *)Next(Folder)) {
if (strncmp(Folder->Folder()->Text(), Path, p - Path) == 0) {
SetCurrent(Folder);
if (Folder->Folder()->SubItems() && strchr(p + 1, FOLDERDELIMCHAR))
AddSubMenu(new cMenuFolder(Title(), Folder->Folder()->SubItems(), nestedItemList, !isempty(dir) ? *cString::sprintf("%s%c%s", *dir, FOLDERDELIMCHAR, Folder->Folder()->Text()) : Folder->Folder()->Text(), p + 1));
break;
}
}
}
}
}
eOSState cMenuFolder::Select(bool Open)
{
if (firstFolder) {
cMenuFolderItem *Folder = (cMenuFolderItem *)Get(Current());
if (Folder) {
if (Open && Folder->Folder()->SubItems())
return AddSubMenu(new cMenuFolder(Title(), Folder->Folder()->SubItems(), nestedItemList, !isempty(dir) ? *cString::sprintf("%s%c%s", *dir, FOLDERDELIMCHAR, Folder->Folder()->Text()) : Folder->Folder()->Text()));
else
return osEnd;
}
}
return osContinue;
}
eOSState cMenuFolder::New(void)
{
editing = true;
return AddSubMenu(new cMenuEditFolder(dir, list));
}
eOSState cMenuFolder::Delete(void)
{
if (!HasSubMenu() && firstFolder) {
cMenuFolderItem *Folder = (cMenuFolderItem *)Get(Current());
if (Folder && Interface->Confirm(Folder->Folder()->SubItems() ? tr("Delete folder and all sub folders?") : tr("Delete folder?"))) {
list->Del(Folder->Folder());
Del(Folder->Index());
firstFolder = Get(isempty(dir) ? 0 : 1);
Display();
SetHelpKeys();
nestedItemList->Save();
}
}
return osContinue;
}
eOSState cMenuFolder::Edit(void)
{
if (!HasSubMenu() && firstFolder) {
cMenuFolderItem *Folder = (cMenuFolderItem *)Get(Current());
if (Folder) {
editing = true;
return AddSubMenu(new cMenuEditFolder(dir, list, Folder->Folder()));
}
}
return osContinue;
}
eOSState cMenuFolder::SetFolder(void)
{
if (cMenuEditFolder *mef = dynamic_cast<cMenuEditFolder *>(SubMenu())) {
Set(mef->GetFolder());
SetHelpKeys();
Display();
nestedItemList->Save();
}
return CloseSubMenu();
}
cString cMenuFolder::GetFolder(void)
{
if (firstFolder) {
cMenuFolderItem *Folder = (cMenuFolderItem *)Get(Current());
if (Folder) {
if (cMenuFolder *mf = dynamic_cast<cMenuFolder *>(SubMenu()))
return cString::sprintf("%s%c%s", Folder->Folder()->Text(), FOLDERDELIMCHAR, *mf->GetFolder());
return Folder->Folder()->Text();
}
}
return "";
}
eOSState cMenuFolder::ProcessKey(eKeys Key)
{
if (!HasSubMenu())
editing = false;
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
switch (Key) {
case kOk: return Select(false);
case kRed: return Select(true);
case kGreen: return New();
case kYellow: return Delete();
case kBlue: return Edit();
default: state = osContinue;
}
}
else if (state == osEnd && HasSubMenu() && editing)
state = SetFolder();
SetHelpKeys();
return state;
}
// --- cMenuEditTimer --------------------------------------------------------
const cTimer *cMenuEditTimer::addedTimer = NULL;
cMenuEditTimer::cMenuEditTimer(cTimer *Timer, bool New)
:cOsdMenu(tr("Edit timer"), 12)
{
SetMenuCategory(mcTimerEdit);
addedTimer = NULL;
file = NULL;
day = 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));
Add(new cMenuEditChanItem(tr("Channel"), &channel));
Add(day = 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));
Add(file = new cMenuEditStrItem( tr("File"), data.file, sizeof(data.file)));
SetFirstDayItem();
if (data.remote)
strn0cpy(remote, data.remote, sizeof(remote));
else
*remote = 0;
if (GetSVDRPServerNames(&svdrpServerNames)) {
svdrpServerNames.Sort(true);
svdrpServerNames.Insert(strdup(""));
Add(new cMenuEditStrlItem(tr("Record on"), remote, sizeof(remote), &svdrpServerNames));
}
}
SetHelpKeys();
}
cMenuEditTimer::~cMenuEditTimer()
{
if (timer && addIfConfirmed)
delete timer; // apparently it wasn't confirmed
}
const cTimer *cMenuEditTimer::AddedTimer(void)
{
const cTimer *Timer = addedTimer;
addedTimer = NULL;
return Timer;
}
void cMenuEditTimer::SetHelpKeys(void)
{
SetHelp(tr("Button$Folder"), data.weekdays ? tr("Button$Single") : tr("Button$Repeating"));
}
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::SetFolder(void)
{
if (cMenuFolder *mf = dynamic_cast<cMenuFolder *>(SubMenu())) {
cString Folder = mf->GetFolder();
char *p = strrchr(data.file, FOLDERDELIMCHAR);
if (p)
p++;
else
p = data.file;
if (!isempty(*Folder))
strn0cpy(data.file, cString::sprintf("%s%c%s", *Folder, FOLDERDELIMCHAR, p), sizeof(data.file));
else if (p != data.file)
memmove(data.file, p, strlen(p) + 1);
SetCurrent(file);
Display();
}
return CloseSubMenu();
}
static bool RemoteTimerError(const cTimer *Timer)
{
Skins.Message(mtError, cString::sprintf("%s %d@%s!", tr("Error while accessing remote timer"), Timer->Id(), Timer->Remote()));
return false; // convenience return code
}
static bool HandleRemoteModifications(cTimer *NewTimer, cTimer *OldTimer = NULL)
{
cStringList Response;
if (!OldTimer || OldTimer->Local() || !OldTimer->Id()) {
if (NewTimer->Local()) { // timer stays local, nothing to do
if (OldTimer && OldTimer->Id())
isyslog("modified timer %s", *NewTimer->ToDescr());
else
isyslog("added timer %s", *NewTimer->ToDescr());
}
else { // timer is new, or moved from local to remote
if (!ExecSVDRPCommand(NewTimer->Remote(), cString::sprintf("NEWT %s", *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
return RemoteTimerError(NewTimer);
int RemoteId = atoi(SVDRPValue(Response[0]));
if (RemoteId <= 0)
return RemoteTimerError(NewTimer);
NewTimer->SetId(RemoteId);
if (OldTimer && OldTimer->Id()) {
if (OldTimer->Recording())
cRecordControls::Stop(OldTimer);
isyslog("moved timer %d to %s", OldTimer->Id(), *NewTimer->ToDescr());
}
else
isyslog("added timer %s", *NewTimer->ToDescr());
}
}
else if (NewTimer->Local()) { // timer is moved from remote to local
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
return RemoteTimerError(OldTimer);
NewTimer->SetId(cTimers::NewTimerId());
NewTimer->ClrFlags(tfRecording); // in case it was recording on the remote machine
isyslog("moved timer %d@%s to %s", OldTimer->Id(), OldTimer->Remote(), *NewTimer->ToDescr());
}
else if (strcmp(OldTimer->Remote(), NewTimer->Remote()) == 0) { // timer stays remote on same machine
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("MODT %d %s", OldTimer->Id(), *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
return RemoteTimerError(NewTimer);
isyslog("modified timer %s", *NewTimer->ToDescr());
}
else { // timer is moved from one remote machine to an other
if (!ExecSVDRPCommand(NewTimer->Remote(), cString::sprintf("NEWT %s", *NewTimer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
return RemoteTimerError(NewTimer);
int RemoteId = atoi(SVDRPValue(Response[0]));
if (RemoteId <= 0)
return RemoteTimerError(NewTimer);
NewTimer->SetId(RemoteId);
if (!ExecSVDRPCommand(OldTimer->Remote(), cString::sprintf("DELT %d", OldTimer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
return RemoteTimerError(OldTimer);
isyslog("moved timer %d@%s to %s", OldTimer->Id(), OldTimer->Remote(), *NewTimer->ToDescr());
}
return true;
}
eOSState cMenuEditTimer::ProcessKey(eKeys Key)
{
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
switch (Key) {
case kOk: if (timer) {
LOCK_TIMERS_WRITE;
if (!addIfConfirmed && !Timers->Contains(timer)) {
Skins.Message(mtWarning, tr("Timer has been deleted!"));
break;
}
LOCK_CHANNELS_READ;
if (const cChannel *Channel = Channels->GetByNumber(channel))
data.channel = Channel;
else {
Skins.Message(mtError, tr("*** Invalid Channel ***"));
break;
}
if (!*data.file)
strcpy(data.file, data.Channel()->ShortName(true));
data.SetRemote(*remote ? remote : NULL);
if (addIfConfirmed) {
*timer = data;
Timers->Add(timer);
addedTimer = timer;
if (!HandleRemoteModifications(timer)) {
// must add the timer before HandleRemoteModifications to get proper log messages with timer ids
Timers->Del(timer);
addedTimer = NULL;
return osContinue;
}
}
else {
if (!HandleRemoteModifications(&data, timer))
return osContinue;
*timer = data;
}
LOCK_SCHEDULES_READ;
timer->SetEventFromSchedule(Schedules);
timer->Matches();
addIfConfirmed = false;
}
return osBack;
case kRed: return AddSubMenu(new cMenuFolder(tr("Select folder"), &Folders, data.file));
case kGreen: if (day) {
day->ToggleRepeating();
SetCurrent(day);
SetFirstDayItem();
SetHelpKeys();
Display();
}
return osContinue;
case kYellow:
case kBlue: return osContinue;
default: break;
}
}
else if (state == osEnd && HasSubMenu())
state = SetFolder();
if (Key != kNone)
SetFirstDayItem();
return state;
}
// --- cMenuTimerItem --------------------------------------------------------
class cMenuTimerItem : public cOsdItem {
private:
const cTimer *timer;
public:
cMenuTimerItem(const cTimer *Timer);
virtual int Compare(const cListObject &ListObject) const;
virtual void Set(void);
const cTimer *Timer(void) { return timer; }
virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable);
};
cMenuTimerItem::cMenuTimerItem(const cTimer *Timer)
{
timer = Timer;
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;
}
const char *File = Setup.FoldersInTimerMenu ? NULL : strrchr(timer->File(), FOLDERDELIMCHAR);
if (File && strcmp(File + 1, TIMERMACRO_TITLE) && strcmp(File + 1, TIMERMACRO_EPISODE))
File++;
else
File = timer->File();
SetText(cString::sprintf("%c\t%d\t%s%s%s\t%02d:%02d\t%02d:%02d\t%s%s",
!(timer->HasFlags(tfActive)) ? ' ' : timer->FirstDay() ? '!' : timer->Recording() ? '#' : '>',
timer->Channel()->Number(),
*name,
*name && **name ? " " : "",
*day,
timer->Start() / 100,
timer->Start() % 100,
timer->Stop() / 100,
timer->Stop() % 100,
timer->Remote() ? *cString::sprintf("@%s: ", timer->Remote()) : "",
File));
}
void cMenuTimerItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
{
if (!DisplayMenu->SetItemTimer(timer, Index, Current, Selectable))
DisplayMenu->SetItem(Text(), Index, Current, Selectable);
}
// --- cMenuTimers -----------------------------------------------------------
class cMenuTimers : public cOsdMenu {
private:
cStateKey timersStateKey;
int helpKeys;
void Set(void);
eOSState Edit(void);
eOSState New(void);
eOSState Delete(void);
eOSState OnOff(void);
eOSState Info(void);
cTimer *GetTimer(void);
void SetHelpKeys(void);
public:
cMenuTimers(void);
virtual ~cMenuTimers();
virtual eOSState ProcessKey(eKeys Key);
};
cMenuTimers::cMenuTimers(void)
:cOsdMenu(tr("Timers"), 2, CHNUMWIDTH, 10, 6, 6)
{
SetMenuCategory(mcTimer);
helpKeys = -1;
cMenuEditTimer::AddedTimer(); // to clear any leftovers
Set();
}
cMenuTimers::~cMenuTimers()
{
}
void cMenuTimers::Set(void)
{
if (const cTimers *Timers = cTimers::GetTimersRead(timersStateKey)) {
const cTimer *CurrentTimer = GetTimer();
cMenuTimerItem *CurrentItem = NULL;
Clear();
for (const cTimer *Timer = Timers->First(); Timer; Timer = Timers->Next(Timer)) {
cMenuTimerItem *Item = new cMenuTimerItem(Timer);
Add(Item);
if (CurrentTimer && Timer->Id() == CurrentTimer->Id() && (!Timer->Remote() && !CurrentTimer->Remote() || Timer->Remote() && CurrentTimer->Remote() && strcmp(Timer->Remote(), CurrentTimer->Remote()) == 0))
CurrentItem = Item;
}
Sort();
SetCurrent(CurrentItem ? CurrentItem : First());
SetHelpKeys();
Display();
timersStateKey.Remove();
}
}
cTimer *cMenuTimers::GetTimer(void)
{
cMenuTimerItem *item = (cMenuTimerItem *)Get(Current());
return item ? (cTimer *)item->Timer() : NULL;
}
void cMenuTimers::SetHelpKeys(void)
{
int NewHelpKeys = 0;
if (const cTimer *Timer = GetTimer()) {
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;
cTimers::GetTimersWrite(timersStateKey);
cTimer *Timer = GetTimer();
if (Timer) {
Timer->OnOff();
if (Timer->Remote()) {
cStringList Response;
if (!ExecSVDRPCommand(Timer->Remote(), cString::sprintf("MODT %d %s", Timer->Id(), *Timer->ToText(true)), &Response) || SVDRPCode(Response[0]) != 250)
RemoteTimerError(Timer);
}
LOCK_SCHEDULES_READ;
Timer->SetEventFromSchedule(Schedules);
RefreshCurrent();
DisplayCurrent(true);
if (Timer->FirstDay())
isyslog("set first day of timer %s to %s", *Timer->ToDescr(), *Timer->PrintFirstDay());
else
isyslog("%sactivated timer %s", Timer->HasFlags(tfActive) ? "" : "de", *Timer->ToDescr());
}
timersStateKey.Remove(Timer != NULL);
return osContinue;
}
eOSState cMenuTimers::Edit(void)
{
if (HasSubMenu() || Count() == 0)
return osContinue;
return AddSubMenu(new cMenuEditTimer(GetTimer()));
}
eOSState cMenuTimers::New(void)
{
if (HasSubMenu())
return osContinue;
cTimer *Timer = new cTimer;
if (*Setup.SVDRPDefaultHost)
Timer->SetRemote(Setup.SVDRPDefaultHost);
return AddSubMenu(new cMenuEditTimer(Timer, true));
}
eOSState cMenuTimers::Delete(void)
{
cTimers *Timers = cTimers::GetTimersWrite(timersStateKey);
// Check if this timer is active:
cTimer *Timer = GetTimer();
if (Timer) {
if (Interface->Confirm(tr("Delete timer?"))) {
if (Timer->Recording()) {
if (Interface->Confirm(tr("Timer still recording - really delete?"))) {
if (!Timer->Remote()) {
Timer->Skip();
cRecordControls::Process(Timers, time(NULL));
}
}
else
Timer = NULL;
}
if (Timer) {
if (Timer->Remote()) {
cStringList Response;
if (!ExecSVDRPCommand(Timer->Remote(), cString::sprintf("DELT %d", Timer->Id()), &Response) || SVDRPCode(Response[0]) != 250)
RemoteTimerError(Timer);
}
Timers->Del(Timer);
cOsdMenu::Del(Current());
Display();
isyslog("deleted timer %s", *Timer->ToDescr());
}
}
}
timersStateKey.Remove(Timer != NULL);
return osContinue;
}
eOSState cMenuTimers::Info(void)
{
if (HasSubMenu() || Count() == 0)
return osContinue;
LOCK_TIMERS_READ;
LOCK_CHANNELS_READ;
cTimer *Timer = GetTimer();
if (Timer && Timer->Event())
return AddSubMenu(new cMenuEvent(Timers, Channels, Timer->Event()));
return osContinue;
}
eOSState cMenuTimers::ProcessKey(eKeys Key)
{
if (!HasSubMenu())
Set();
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
switch (Key) {
case kOk: return Edit();
case kRed: state = OnOff(); break; // must go through SetHelpKeys()!
case kGreen: return New();
case kYellow: state = Delete(); break;
case kInfo:
case kBlue: return Info();
break;
default: break;
}
}
if (const cTimer *Timer = cMenuEditTimer::AddedTimer()) {
// a newly created timer was confirmed with Ok and the proper item needs to be added:
LOCK_TIMERS_READ;
Add(new cMenuTimerItem(Timer), true);
Display();
}
if (Key != kNone)
SetHelpKeys();
return state;
}
// --- cMenuEvent ------------------------------------------------------------
cMenuEvent::cMenuEvent(const cTimers *Timers, const cChannels *Channels, const cEvent *Event, bool CanSwitch, bool Buttons)
:cOsdMenu(tr("Event"))
{
SetMenuCategory(mcEvent);
event = Event;
if (event) {
if (const cChannel *Channel = Channels->GetByChannelID(event->ChannelID(), true)) {
SetTitle(Channel->Name());
if (Buttons) {
eTimerMatch TimerMatch = tmNone;
Timers->GetMatch(event, &TimerMatch);
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);
if (event->Description())
cStatus::MsgOsdTextItem(event->Description());
}
eOSState cMenuEvent::ProcessKey(eKeys Key)
{
switch (int(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;
eTimerMatch timerMatch;
cMenuScheduleItem(const cTimers *Timers, const cEvent *Event, const cChannel *Channel = NULL, bool WithDate = false);
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(const cTimers *Timers, bool Force = false);
virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable);
};
cMenuScheduleItem::eScheduleSortMode cMenuScheduleItem::sortMode = ssmAllThis;
cMenuScheduleItem::cMenuScheduleItem(const cTimers *Timers, const cEvent *Event, const cChannel *Channel, bool WithDate)
{
event = Event;
channel = Channel;
withDate = WithDate;
timerMatch = tmNone;
Update(Timers, 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;
}
static const char *TimerMatchChars = " tT";
bool cMenuScheduleItem::Update(const cTimers *Timers, bool Force)
{
eTimerMatch OldTimerMatch = timerMatch;
Timers->GetMatch(event, &timerMatch);
if (Force || timerMatch != OldTimerMatch) {
cString buffer;
char t = TimerMatchChars[timerMatch];
char v = event->Vps() && (event->Vps() - event->StartTime()) ? 'V' : ' ';
char r = event->SeenWithin(30) && event->IsRunning() ? '*' : ' ';
const char *csn = channel ? channel->ShortName(true) : NULL;
cString eds = event->GetDateString();
if (channel && withDate)
buffer = cString::sprintf("%d\t%.*s\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), Utf8SymChars(csn, 999), csn, Utf8SymChars(eds, 6), *eds, *event->GetTimeString(), t, v, r, event->Title());
else if (channel)
buffer = cString::sprintf("%d\t%.*s\t%s\t%c%c%c\t%s", channel->Number(), Utf8SymChars(csn, 999), csn, *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());
SetText(buffer);
return true;
}
return false;
}
void cMenuScheduleItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
{
if (!DisplayMenu->SetItemEvent(event, Index, Current, Selectable, channel, withDate, timerMatch))
DisplayMenu->SetItem(Text(), Index, Current, Selectable);
}
// --- cMenuWhatsOn ----------------------------------------------------------
class cMenuWhatsOn : public cOsdMenu {
private:
bool now;
bool canSwitch;
int helpKeys;
cStateKey timersStateKey;
eOSState Record(void);
eOSState Switch(void);
static int currentChannel;
static const cEvent *scheduleEvent;
bool Update(void);
void SetHelpKeys(void);
public:
cMenuWhatsOn(const cTimers *Timers, const cChannels *Channels, 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);
};
int cMenuWhatsOn::currentChannel = 0;
const cEvent *cMenuWhatsOn::scheduleEvent = NULL;
cMenuWhatsOn::cMenuWhatsOn(const cTimers *Timers, const cChannels *Channels, const cSchedules *Schedules, bool Now, int CurrentChannelNr)
:cOsdMenu(Now ? tr("What's on now?") : tr("What's on next?"), CHNUMWIDTH, CHNAMWIDTH, 6, 4)
{
SetMenuCategory(Now ? mcScheduleNow : mcScheduleNext);
now = Now;
canSwitch = false;
helpKeys = 0;
for (const cChannel *Channel = Channels->First(); Channel; Channel = Channels->Next(Channel)) {
if (!Channel->GroupSep()) {
if (const cSchedule *Schedule = Schedules->GetSchedule(Channel)) {
if (const cEvent *Event = Now ? Schedule->GetPresentEvent() : Schedule->GetFollowingEvent())
Add(new cMenuScheduleItem(Timers, Event, Channel), Channel->Number() == CurrentChannelNr);
}
}
}
currentChannel = CurrentChannelNr;
Display();
SetHelpKeys();
}
bool cMenuWhatsOn::Update(void)
{
bool result = false;
if (const cTimers *Timers = cTimers::GetTimersRead(timersStateKey)) {
for (cOsdItem *item = First(); item; item = Next(item)) {
if (((cMenuScheduleItem *)item)->Update(Timers))
result = true;
}
timersStateKey.Remove();
}
return result;
}
void cMenuWhatsOn::SetHelpKeys(void)
{
cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
canSwitch = false;
int NewHelpKeys = 0;
if (item) {
if (item->timerMatch == tmFull)
NewHelpKeys |= 0x02; // "Timer"
else
NewHelpKeys |= 0x01; // "Record"
if (now)
NewHelpKeys |= 0x04; // "Next"
else
NewHelpKeys |= 0x08; // "Now"
LOCK_CHANNELS_READ;
if (const cChannel *Channel = Channels->GetByChannelID(item->event->ChannelID(), true)) {
if (Channel->Number() != cDevice::CurrentChannel()) {
NewHelpKeys |= 0x10; // "Switch"
canSwitch = true;
}
}
}
if (NewHelpKeys != helpKeys) {
const char *Red[] = { NULL, tr("Button$Record"), tr("Button$Timer") };
SetHelp(Red[NewHelpKeys & 0x03], now ? tr("Button$Next") : tr("Button$Now"), tr("Button$Schedule"), canSwitch ? tr("Button$Switch") : NULL);
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) {
LOCK_CHANNELS_READ;
const cChannel *Channel = Channels->GetByChannelID(item->event->ChannelID(), true);
if (Channel) {
if (!cDevice::PrimaryDevice()->SwitchChannel(Channel, true))
Channel = NULL;
}
if (Channel)
return osEnd;
}
Skins.Message(mtError, tr("Can't switch channel!"));
return osContinue;
}
eOSState cMenuWhatsOn::Record(void)
{
if (cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current())) {
{
LOCK_TIMERS_WRITE;
LOCK_SCHEDULES_READ;
Timers->SetExplicitModify();
if (item->timerMatch == tmFull) {
if (cTimer *Timer = Timers->GetMatch(item->event))
return AddSubMenu(new cMenuEditTimer(Timer));
}
cTimer *Timer = new cTimer(item->event);
if (*Setup.SVDRPDefaultHost)
Timer->SetRemote(Setup.SVDRPDefaultHost);
if (cTimer *t = Timers->GetTimer(Timer)) {
delete Timer;
Timer = t;
return AddSubMenu(new cMenuEditTimer(Timer));
}
if (Timer->Matches(0, false, NEWTIMERLIMIT))
return AddSubMenu(new cMenuEditTimer(Timer, true));
Timers->Add(Timer);
Timers->SetModified();
if (!HandleRemoteModifications(Timer)) {
// must add the timer before HandleRemoteModifications to get proper log messages with timer ids
Timers->Del(Timer);
delete 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: if (canSwitch)
return Switch();
break;
case kInfo:
case kOk: if (Count()) {
LOCK_TIMERS_READ;
LOCK_CHANNELS_READ;
return AddSubMenu(new cMenuEvent(Timers, Channels, ((cMenuScheduleItem *)Get(Current()))->event, canSwitch, true));
}
break;
default: break;
}
}
else if (!HasSubMenu()) {
if (HadSubMenu && Update())
Display();
if (Key != kNone)
SetHelpKeys();
}
return state;
}
// --- cMenuSchedule ---------------------------------------------------------
class cMenuSchedule : public cOsdMenu {
private:
cStateKey timersStateKey;
cStateKey schedulesStateKey;
int scheduleState;
bool now, next;
bool canSwitch;
int helpKeys;
void Set(const cChannel *Channel = NULL, bool Force = false);
eOSState Number(void);
eOSState Record(void);
eOSState Switch(void);
bool PrepareScheduleAllThis(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel);
bool PrepareScheduleThisThis(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel);
bool PrepareScheduleThisAll(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel);
bool PrepareScheduleAllAll(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel);
bool Update(void);
void SetHelpKeys(void);
public:
cMenuSchedule(void);
virtual ~cMenuSchedule();
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSchedule::cMenuSchedule(void)
:cOsdMenu("")
{
SetMenuCategory(mcSchedule);
scheduleState = -1;
now = next = false;
canSwitch = false;
helpKeys = 0;
cMenuScheduleItem::SetSortMode(cMenuScheduleItem::ssmAllThis);
cMenuWhatsOn::SetCurrentChannel(cDevice::CurrentChannel());
Set(NULL, true);
}
cMenuSchedule::~cMenuSchedule()
{
cMenuWhatsOn::ScheduleEvent(); // makes sure any posted data is cleared
}
void cMenuSchedule::Set(const cChannel *Channel, bool Force)
{
if (Force) {
schedulesStateKey.Reset();
scheduleState = -1;
}
LOCK_TIMERS_READ;
LOCK_CHANNELS_READ;
if (const cSchedules *Schedules = cSchedules::GetSchedulesRead(schedulesStateKey)) {
cMenuScheduleItem *CurrentItem = (cMenuScheduleItem *)Get(Current());
const cEvent *Event = NULL;
if (!Channel) {
if (CurrentItem) {
Event = CurrentItem->event;
Channel = Channels->GetByChannelID(Event->ChannelID(), true);
}
else
Channel = Channels->GetByNumber(cDevice::CurrentChannel());
}
bool Refresh = false;
switch (cMenuScheduleItem::SortMode()) {
case cMenuScheduleItem::ssmAllThis: Refresh = PrepareScheduleAllThis(Timers, Schedules, Event, Channel); break;
case cMenuScheduleItem::ssmThisThis: Refresh = PrepareScheduleThisThis(Timers, Schedules, Event, Channel); break;
case cMenuScheduleItem::ssmThisAll: Refresh = Force && PrepareScheduleThisAll(Timers, Schedules, Event, Channel); break;
case cMenuScheduleItem::ssmAllAll: Refresh = Force && PrepareScheduleAllAll(Timers, Schedules, Event, Channel); break;
default: esyslog("ERROR: unknown SortMode %d (%s %d)", cMenuScheduleItem::SortMode(), __FUNCTION__, __LINE__);
}
if (Refresh) {
CurrentItem = (cMenuScheduleItem *)Get(Current());
Sort();
SetCurrent(CurrentItem);
SetHelpKeys();
Display();
}
schedulesStateKey.Remove();
}
}
bool cMenuSchedule::PrepareScheduleAllThis(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel)
{
if (const cSchedule *Schedule = Schedules->GetSchedule(Channel)) {
if (Schedule->Modified(scheduleState)) {
Clear();
SetCols(7, 6, 4);
SetTitle(cString::sprintf(tr("Schedule - %s"), Channel->Name()));
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(Timers, ev), ev == PresentEvent);
}
return true;
}
}
return false;
}
bool cMenuSchedule::PrepareScheduleThisThis(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel)
{
if (Event) {
if (const cSchedule *Schedule = Schedules->GetSchedule(Channel)) {
if (Schedule->Modified(scheduleState)) {
Clear();
SetCols(7, 6, 4);
SetTitle(cString::sprintf(tr("This event - %s"), Channel->Name()));
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(Timers, ev), ev == Event);
}
return true;
}
}
}
return false;
}
bool cMenuSchedule::PrepareScheduleThisAll(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel)
{
Clear();
SetCols(CHNUMWIDTH, CHNAMWIDTH, 7, 6, 4);
SetTitle(tr("This event - all channels"));
if (Event) {
LOCK_CHANNELS_READ;
for (const cChannel *ch = Channels->First(); ch; ch = Channels->Next(ch)) {
if (const cSchedule *Schedule = Schedules->GetSchedule(ch)) {
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(Timers, ev, ch, true), ev == Event && ch == Channel);
}
}
}
}
return true;
}
bool cMenuSchedule::PrepareScheduleAllAll(const cTimers *Timers, const cSchedules *Schedules, const cEvent *Event, const cChannel *Channel)
{
Clear();
SetCols(CHNUMWIDTH, CHNAMWIDTH, 7, 6, 4);
SetTitle(tr("All events - all channels"));
LOCK_CHANNELS_READ;
cStateKey StateKey;
for (const cChannel *ch = Channels->First(); ch; ch = Channels->Next(ch)) {
if (const cSchedule *Schedule = Schedules->GetSchedule(ch)) {
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(Timers, ev, ch, true), ev == Event && ch == Channel);
}
}
}
return true;
}
bool cMenuSchedule::Update(void)
{
bool result = false;
if (const cTimers *Timers = cTimers::GetTimersRead(timersStateKey)) {
for (cOsdItem *item = First(); item; item = Next(item)) {
if (((cMenuScheduleItem *)item)->Update(Timers))
result = true;
}
timersStateKey.Remove();
}
return result;
}
void cMenuSchedule::SetHelpKeys(void)
{
cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
canSwitch = false;
int NewHelpKeys = 0;
if (item) {
if (item->timerMatch == tmFull)
NewHelpKeys |= 0x02; // "Timer"
else
NewHelpKeys |= 0x01; // "Record"
LOCK_CHANNELS_READ;
if (const cChannel *Channel = Channels->GetByChannelID(item->event->ChannelID(), true)) {
if (Channel->Number() != cDevice::CurrentChannel()) {
NewHelpKeys |= 0x10; // "Switch"
canSwitch = true;
}
}
}
if (NewHelpKeys != helpKeys) {
const char *Red[] = { NULL, tr("Button$Record"), tr("Button$Timer") };
SetHelp(Red[NewHelpKeys & 0x03], tr("Button$Now"), tr("Button$Next"), canSwitch ? tr("Button$Switch") : NULL);
helpKeys = NewHelpKeys;
}
}
eOSState cMenuSchedule::Number(void)
{
cMenuScheduleItem::IncSortMode();
Set(NULL, true);
return osContinue;
}
eOSState cMenuSchedule::Record(void)
{
if (cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current())) {
{
LOCK_TIMERS_WRITE;
LOCK_SCHEDULES_READ;
Timers->SetExplicitModify();
if (item->timerMatch == tmFull) {
if (cTimer *Timer = Timers->GetMatch(item->event))
return AddSubMenu(new cMenuEditTimer(Timer));
}
cTimer *Timer = new cTimer(item->event);
if (*Setup.SVDRPDefaultHost)
Timer->SetRemote(Setup.SVDRPDefaultHost);
if (cTimer *t = Timers->GetTimer(Timer)) {
delete Timer;
Timer = t;
return AddSubMenu(new cMenuEditTimer(Timer));
}
if (Timer->Matches(0, false, NEWTIMERLIMIT))
return AddSubMenu(new cMenuEditTimer(Timer, true));
Timers->Add(Timer);
Timers->SetModified();
if (!HandleRemoteModifications(Timer)) {
// must add the timer before HandleRemoteModifications to get proper log messages with timer ids
Timers->Del(Timer);
delete Timer;
}
}
if (HasSubMenu())
CloseSubMenu();
if (Update())
Display();
SetHelpKeys();
}
return osContinue;
}
eOSState cMenuSchedule::Switch(void)
{
cMenuScheduleItem *item = (cMenuScheduleItem *)Get(Current());
if (item) {
LOCK_CHANNELS_READ;
const cChannel *Channel = NULL;
if (Channel = Channels->GetByChannelID(item->event->ChannelID(), true)) {
if (!Channels->SwitchTo(Channel->Number()))
Channel = NULL;
}
if (Channel)
return osEnd;
}
Skins.Message(mtError, tr("Can't switch channel!"));
return osContinue;
}
eOSState cMenuSchedule::ProcessKey(eKeys Key)
{
if (!HasSubMenu())
Set(); // react on any changes to the schedules list
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: {
LOCK_TIMERS_READ;
LOCK_CHANNELS_READ;
LOCK_SCHEDULES_READ;
if (!now && !next) {
int ChannelNr = 0;
if (Count()) {
if (const cChannel *Channel = Channels->GetByChannelID(((cMenuScheduleItem *)Get(Current()))->event->ChannelID(), true))
ChannelNr = Channel->Number();
}
now = true;
return AddSubMenu(new cMenuWhatsOn(Timers, Channels, Schedules, now, ChannelNr));
}
now = !now;
next = !next;
return AddSubMenu(new cMenuWhatsOn(Timers, Channels, Schedules, now, cMenuWhatsOn::CurrentChannel()));
}
case kYellow: {
LOCK_TIMERS_READ;
LOCK_CHANNELS_READ;
LOCK_SCHEDULES_READ;
return AddSubMenu(new cMenuWhatsOn(Timers, Channels, Schedules, false, cMenuWhatsOn::CurrentChannel()));
}
case kBlue: if (canSwitch)
return Switch();
break;
case kInfo:
case kOk: if (Count()) {
LOCK_TIMERS_READ;
LOCK_CHANNELS_READ;
LOCK_SCHEDULES_READ;
return AddSubMenu(new cMenuEvent(Timers, Channels, ((cMenuScheduleItem *)Get(Current()))->event, canSwitch, true));
}
break;
default: break;
}
}
else if (!HasSubMenu()) {
now = next = false;
if (const cEvent *ei = cMenuWhatsOn::ScheduleEvent()) {
LOCK_CHANNELS_READ;
if (const cChannel *Channel = Channels->GetByChannelID(ei->ChannelID(), true)) {
cMenuScheduleItem::SetSortMode(cMenuScheduleItem::ssmAllThis);
Set(Channel, true);
}
}
else if (HadSubMenu && Update())
Display();
if (Key != kNone)
SetHelpKeys();
}
return state;
}
// --- cMenuCommands ---------------------------------------------------------
cMenuCommands::cMenuCommands(const char *Title, cList<cNestedItem> *Commands, const char *Parameters)
:cOsdMenu(Title)
{
SetMenuCategory(mcCommand);
result = NULL;
SetHasHotkeys();
commands = Commands;
parameters = Parameters;
for (cNestedItem *Command = commands->First(); Command; Command = commands->Next(Command)) {
const char *s = Command->Text();
if (Command->SubItems())
Add(new cOsdItem(hk(cString::sprintf("%s...", s))));
else if (Parse(s))
Add(new cOsdItem(hk(title)));
}
}
cMenuCommands::~cMenuCommands()
{
free(result);
}
bool cMenuCommands::Parse(const char *s)
{
const char *p = strchr(s, ':');
if (p) {
int l = p - s;
if (l > 0) {
char t[l + 1];
stripspace(strn0cpy(t, s, l + 1));
l = strlen(t);
if (l > 1 && t[l - 1] == '?') {
t[l - 1] = 0;
confirm = true;
}
else
confirm = false;
title = t;
command = skipspace(p + 1);
return true;
}
}
return false;
}
eOSState cMenuCommands::Execute(void)
{
cNestedItem *Command = commands->Get(Current());
if (Command) {
if (Command->SubItems())
return AddSubMenu(new cMenuCommands(Title(), Command->SubItems(), parameters));
if (Parse(Command->Text())) {
if (!confirm || Interface->Confirm(cString::sprintf("%s?", *title))) {
Skins.Message(mtStatus, cString::sprintf("%s...", *title));
free(result);
result = NULL;
cString cmdbuf;
if (!isempty(parameters))
cmdbuf = cString::sprintf("%s %s", *command, *parameters);
const char *cmd = *cmdbuf ? *cmdbuf : *command;
dsyslog("executing command '%s'", cmd);
cPipe p;
if (p.Open(cmd, "r")) {
int l = 0;
int c;
while ((c = fgetc(p)) != EOF) {
if (l % 20 == 0) {
if (char *NewBuffer = (char *)realloc(result, l + 21))
result = NewBuffer;
else {
esyslog("ERROR: out of memory");
break;
}
}
result[l++] = char(c);
}
if (result)
result[l] = 0;
p.Close();
}
else
esyslog("ERROR: can't open pipe for command '%s'", cmd);
Skins.Message(mtStatus, NULL);
if (result)
return AddSubMenu(new cMenuText(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 --------------------------------------------------------------
static bool CamMenuIsOpen = false;
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);
};
cMenuCam::cMenuCam(cCamSlot *CamSlot)
:cOsdMenu("", 1) // tab necessary for enquiry!
{
SetMenuCategory(mcCam);
camSlot = CamSlot;
ciMenu = NULL;
ciEnquiry = NULL;
input = NULL;
offset = 0;
lastCamExchange = time(NULL);
SetNeedsFastResponse(true);
QueryCam();
CamMenuIsOpen = true;
}
cMenuCam::~cMenuCam()
{
if (ciMenu)
ciMenu->Abort();
delete ciMenu;
if (ciEnquiry)
ciEnquiry->Abort();
delete ciEnquiry;
free(input);
CamMenuIsOpen = false;
}
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;
}
bool CamMenuActive(void)
{
return CamMenuIsOpen;
}
// --- cMenuPathEdit ---------------------------------------------------------
class cMenuPathEdit : public cOsdMenu {
private:
cString path;
char folder[PATH_MAX];
char name[NAME_MAX];
cMenuEditStrItem *folderItem;
int pathIsInUse;
eOSState SetFolder(void);
eOSState Folder(void);
eOSState ApplyChanges(void);
public:
cMenuPathEdit(const char *Path);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuPathEdit::cMenuPathEdit(const char *Path)
:cOsdMenu(tr("Edit path"), 12)
{
SetMenuCategory(mcRecordingEdit);
path = Path;
*folder = 0;
*name = 0;
const char *s = strrchr(path, FOLDERDELIMCHAR);
if (s) {
strn0cpy(folder, cString(path, s), sizeof(folder));
s++;
}
else
s = path;
strn0cpy(name, s, sizeof(name));
{
LOCK_RECORDINGS_READ;
pathIsInUse = Recordings->PathIsInUse(path);
}
cOsdItem *p;
Add(p = folderItem = new cMenuEditStrItem(tr("Folder"), folder, sizeof(folder)));
p->SetSelectable(!pathIsInUse);
Add(p = new cMenuEditStrItem(tr("Name"), name, sizeof(name)));
p->SetSelectable(!pathIsInUse);
if (pathIsInUse) {
Add(new cOsdItem("", osUnknown, false));
Add(new cOsdItem(tr("This folder is currently in use - no changes are possible!"), osUnknown, false));
}
Display();
if (!pathIsInUse)
SetHelp(tr("Button$Folder"));
}
eOSState cMenuPathEdit::SetFolder(void)
{
if (cMenuFolder *mf = dynamic_cast<cMenuFolder *>(SubMenu())) {
strn0cpy(folder, mf->GetFolder(), sizeof(folder));
SetCurrent(folderItem);
Display();
}
return CloseSubMenu();
}
eOSState cMenuPathEdit::Folder(void)
{
return AddSubMenu(new cMenuFolder(tr("Select folder"), &Folders, path));
}
eOSState cMenuPathEdit::ApplyChanges(void)
{
if (!*name) {
*name = ' '; // name must not be empty!
name[1] = 0;
}
cString NewPath = *folder ? cString::sprintf("%s%c%s", folder, FOLDERDELIMCHAR, name) : name;
NewPath.CompactChars(FOLDERDELIMCHAR);
if (strcmp(NewPath, path)) {
LOCK_RECORDINGS_WRITE;
Recordings->SetExplicitModify();
int NumRecordings = Recordings->GetNumRecordingsInPath(path);
if (NumRecordings > 1 && !Interface->Confirm(cString::sprintf(tr("Move entire folder containing %d recordings?"), NumRecordings)))
return osContinue;
if (!Recordings->MoveRecordings(path, NewPath)) {
Skins.Message(mtError, tr("Error while moving folder!"));
return osContinue;
}
cMenuRecordings::SetPath(NewPath); // makes sure the Recordings menu will reposition to the new path
Recordings->SetModified();
return osUser1;
}
return osBack;
}
eOSState cMenuPathEdit::ProcessKey(eKeys Key)
{
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
if (!pathIsInUse) {
switch (Key) {
case kRed: return Folder();
case kOk: return ApplyChanges();
default: break;
}
}
else if (Key == kOk)
return osBack;
}
else if (state == osEnd && HasSubMenu())
state = SetFolder();
return state;
}
// --- cMenuRecordingEdit ----------------------------------------------------
class cMenuRecordingEdit : public cOsdMenu {
private:
const cRecording *recording;
cString originalFileName;
cStateKey recordingsStateKey;
char folder[PATH_MAX];
char name[NAME_MAX];
int priority;
int lifetime;
cMenuEditStrItem *folderItem;
cMenuEditStrItem *nameItem;
const char *buttonFolder;
const char *buttonAction;
const char *buttonDeleteMarks;
const char *actionCancel;
const char *doCut;
int recordingIsInUse;
void Set(void);
void SetHelpKeys(void);
bool RefreshRecording(void);
eOSState SetFolder(void);
eOSState Folder(void);
eOSState Action(void);
eOSState RemoveName(void);
eOSState DeleteMarks(void);
eOSState ApplyChanges(void);
public:
cMenuRecordingEdit(const cRecording *Recording);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuRecordingEdit::cMenuRecordingEdit(const cRecording *Recording)
:cOsdMenu(tr("Edit recording"), 12)
{
SetMenuCategory(mcRecordingEdit);
recording = Recording;
originalFileName = recording->FileName();
strn0cpy(folder, recording->Folder(), sizeof(folder));
strn0cpy(name, recording->BaseName(), sizeof(name));
priority = recording->Priority();
lifetime = recording->Lifetime();
folderItem = NULL;
nameItem = NULL;
buttonFolder = NULL;
buttonAction = NULL;
buttonDeleteMarks = NULL;
actionCancel = NULL;
doCut = NULL;
recordingIsInUse = ruNone;
Set();
}
void cMenuRecordingEdit::Set(void)
{
int current = Current();
Clear();
recordingIsInUse = recording->IsInUse();
cOsdItem *p;
Add(p = folderItem = new cMenuEditStrItem(tr("Folder"), folder, sizeof(folder)));
p->SetSelectable(!recordingIsInUse);
Add(p = nameItem = new cMenuEditStrItem(tr("Name"), name, sizeof(name)));
p->SetSelectable(!recordingIsInUse);
Add(p = new cMenuEditIntItem(tr("Priority"), &priority, 0, MAXPRIORITY));
p->SetSelectable(!recordingIsInUse);
Add(p = new cMenuEditIntItem(tr("Lifetime"), &lifetime, 0, MAXLIFETIME));
p->SetSelectable(!recordingIsInUse);
if (recordingIsInUse) {
Add(new cOsdItem("", osUnknown, false));
Add(new cOsdItem(tr("This recording is currently in use - no changes are possible!"), osUnknown, false));
}
SetCurrent(Get(current));
Display();
SetHelpKeys();
}
void cMenuRecordingEdit::SetHelpKeys(void)
{
buttonFolder = !recordingIsInUse ? tr("Button$Folder") : NULL;
buttonAction = NULL;
buttonDeleteMarks = NULL;
actionCancel = NULL;
doCut = NULL;
if ((recordingIsInUse & ruCut) != 0)
buttonAction = actionCancel = ((recordingIsInUse & ruPending) != 0) ? tr("Button$Cancel cutting") : tr("Button$Stop cutting");
else if ((recordingIsInUse & ruMove) != 0)
buttonAction = actionCancel = ((recordingIsInUse & ruPending) != 0) ? tr("Button$Cancel moving") : tr("Button$Stop moving");
else if ((recordingIsInUse & ruCopy) != 0)
buttonAction = actionCancel = ((recordingIsInUse & ruPending) != 0) ? tr("Button$Cancel copying") : tr("Button$Stop copying");
else if (recording->HasMarks()) {
buttonAction = doCut = tr("Button$Cut");
buttonDeleteMarks = tr("Button$Delete marks");
}
SetHelp(buttonFolder, buttonAction, buttonDeleteMarks);
}
bool cMenuRecordingEdit::RefreshRecording(void)
{
if (const cRecordings *Recordings = cRecordings::GetRecordingsRead(recordingsStateKey)) {
if ((recording = Recordings->GetByName(originalFileName)) != NULL)
Set();
else {
recordingsStateKey.Remove();
Skins.Message(mtWarning, tr("Recording vanished!"));
return false;
}
recordingsStateKey.Remove();
}
return true;
}
eOSState cMenuRecordingEdit::SetFolder(void)
{
if (cMenuFolder *mf = dynamic_cast<cMenuFolder *>(SubMenu())) {
strn0cpy(folder, mf->GetFolder(), sizeof(folder));
SetCurrent(folderItem);
Display();
}
return CloseSubMenu();
}
eOSState cMenuRecordingEdit::Folder(void)
{
return AddSubMenu(new cMenuFolder(tr("Select folder"), &Folders, recording->Name()));
}
eOSState cMenuRecordingEdit::Action(void)
{
if (actionCancel)
RecordingsHandler.Del(recording->FileName());
else if (doCut) {
if (access(cCutter::EditedFileName(recording->FileName()), F_OK) != 0 || Interface->Confirm(tr("Edited version already exists - overwrite?"))) {
if (!RecordingsHandler.Add(ruCut, recording->FileName()))
Skins.Message(mtError, tr("Error while queueing recording for cutting!"));
}
}
recordingIsInUse = recording->IsInUse();
RefreshRecording();
SetHelpKeys();
return osContinue;
}
eOSState cMenuRecordingEdit::RemoveName(void)
{
if (Get(Current()) == nameItem) {
if (Interface->Confirm(tr("Rename recording to folder name?"))) {
char *s = strrchr(folder, FOLDERDELIMCHAR);
if (s)
*s++ = 0;
else
s = folder;
strn0cpy(name, s, sizeof(name));
if (s == folder)
*s = 0;
Set();
}
}
return osContinue;
}
eOSState cMenuRecordingEdit::DeleteMarks(void)
{
if (buttonDeleteMarks && Interface->Confirm(tr("Delete editing marks for this recording?"))) {
if (cMarks::DeleteMarksFile(recording))
SetHelpKeys();
else
Skins.Message(mtError, tr("Error while deleting editing marks!"));
}
return osContinue;
}
eOSState cMenuRecordingEdit::ApplyChanges(void)
{
cStateKey StateKey;
cRecordings *Recordings = cRecordings::GetRecordingsWrite(StateKey);
cRecording *Recording = Recordings->GetByName(recording->FileName());
if (!Recording) {
Skins.Message(mtWarning, tr("Recording vanished!"));
return osBack;
}
bool Modified = false;
if (priority != recording->Priority() || lifetime != recording->Lifetime()) {
if (!Recording->ChangePriorityLifetime(priority, lifetime)) {
Skins.Message(mtError, tr("Error while changing priority/lifetime!"));
StateKey.Remove(Modified);
return osContinue;
}
Modified = true;
}
if (!*name) {
*name = ' '; // name must not be empty!
name[1] = 0;
}
cString NewName = *folder ? cString::sprintf("%s%c%s", folder, FOLDERDELIMCHAR, name) : name;
NewName.CompactChars(FOLDERDELIMCHAR);
if (strcmp(NewName, Recording->Name())) {
if (!Recording->ChangeName(NewName)) {
Skins.Message(mtError, tr("Error while changing folder/name!"));
StateKey.Remove(Modified);
return osContinue;
}
Modified = true;
}
if (Modified) {
cMenuRecordings::SetRecording(Recording->FileName()); // makes sure the Recordings menu will reposition to the renamed recording
Recordings->TouchUpdate();
StateKey.Remove(Modified);
return osUser1;
}
StateKey.Remove(Modified);
return osBack;
}
eOSState cMenuRecordingEdit::ProcessKey(eKeys Key)
{
if (!HasSubMenu()) {
if (!RefreshRecording())
return osBack; // the recording has vanished, so close this menu
}
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
switch (Key) {
case k0: return RemoveName();
case kRed: return buttonFolder ? Folder() : osContinue;
case kGreen: return buttonAction ? Action() : osContinue;
case kYellow: return buttonDeleteMarks ? DeleteMarks() : osContinue;
case kOk: return !recordingIsInUse ? ApplyChanges() : osBack;
default: break;
}
}
else if (state == osEnd && HasSubMenu())
state = SetFolder();
return state;
}
// --- cMenuRecording --------------------------------------------------------
class cMenuRecording : public cOsdMenu {
private:
const cRecording *recording;
cString originalFileName;
cStateKey recordingsStateKey;
bool withButtons;
bool RefreshRecording(void);
public:
cMenuRecording(const cRecording *Recording, bool WithButtons = false);
virtual void Display(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuRecording::cMenuRecording(const cRecording *Recording, bool WithButtons)
:cOsdMenu(tr("Recording info"))
{
SetMenuCategory(mcRecordingInfo);
recording = Recording;
originalFileName = recording->FileName();
withButtons = WithButtons;
if (withButtons)
SetHelp(tr("Button$Play"), tr("Button$Rewind"), NULL, tr("Button$Edit"));
}
bool cMenuRecording::RefreshRecording(void)
{
if (const cRecordings *Recordings = cRecordings::GetRecordingsRead(recordingsStateKey)) {
if ((recording = Recordings->GetByName(originalFileName)) != NULL)
Display();
else {
recordingsStateKey.Remove();
Skins.Message(mtWarning, tr("Recording vanished!"));
return false;
}
recordingsStateKey.Remove();
}
return true;
}
void cMenuRecording::Display(void)
{
if (HasSubMenu()) {
SubMenu()->Display();
return;
}
cOsdMenu::Display();
DisplayMenu()->SetRecording(recording);
if (recording->Info()->Description())
cStatus::MsgOsdTextItem(recording->Info()->Description());
}
eOSState cMenuRecording::ProcessKey(eKeys Key)
{
if (HasSubMenu())
return cOsdMenu::ProcessKey(Key);
else if (!RefreshRecording())
return osBack; // the recording has vanished, so close this menu
switch (int(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;
case kBlue: if (withButtons)
return AddSubMenu(new cMenuRecordingEdit(recording));
break;
default: break;
}
}
return state;
}
// --- cMenuRecordingItem ----------------------------------------------------
class cMenuRecordingItem : public cOsdItem {
private:
const cRecording *recording;
int level;
char *name;
int totalEntries, newEntries;
public:
cMenuRecordingItem(const cRecording *Recording, int Level);
~cMenuRecordingItem();
void IncrementCounter(bool New);
const char *Name(void) { return name; }
int Level(void) { return level; }
const cRecording *Recording(void) { return recording; }
bool IsDirectory(void) { return name != NULL; }
void SetRecording(const cRecording *Recording) { recording = Recording; }
virtual void SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable);
};
cMenuRecordingItem::cMenuRecordingItem(const cRecording *Recording, int Level)
{
recording = Recording;
level = Level;
name = NULL;
totalEntries = newEntries = 0;
SetText(Recording->Title('\t', true, Level));
if (*Text() == '\t')
name = strdup(Text() + 2); // 'Text() + 2' to skip the two '\t'
}
cMenuRecordingItem::~cMenuRecordingItem()
{
free(name);
}
void cMenuRecordingItem::IncrementCounter(bool New)
{
totalEntries++;
if (New)
newEntries++;
SetText(cString::sprintf("%d\t\t%d\t%s", totalEntries, newEntries, name));
}
void cMenuRecordingItem::SetMenuItem(cSkinDisplayMenu *DisplayMenu, int Index, bool Current, bool Selectable)
{
if (!DisplayMenu->SetItemRecording(recording, Index, Current, Selectable, level, totalEntries, newEntries))
DisplayMenu->SetItem(Text(), Index, Current, Selectable);
}
// --- cMenuRecordings -------------------------------------------------------
cString cMenuRecordings::path;
cString cMenuRecordings::fileName;
cMenuRecordings::cMenuRecordings(const char *Base, int Level, bool OpenSubMenus, const cRecordingFilter *Filter)
:cOsdMenu(Base ? Base : tr("Recordings"), 9, 6, 6)
{
SetMenuCategory(mcRecording);
base = Base ? strdup(Base) : NULL;
level = Setup.RecordingDirs ? Level : -1;
filter = Filter;
helpKeys = -1;
Display(); // this keeps the higher level menus from showing up briefly when pressing 'Back' during replay
Set();
if (Current() < 0)
SetCurrent(First());
else if (OpenSubMenus && (cReplayControl::LastReplayed() || *path || *fileName)) {
if (!*path || Level < strcountchr(path, FOLDERDELIMCHAR)) {
if (Open(true))
return;
}
}
Display();
SetHelpKeys();
}
cMenuRecordings::~cMenuRecordings()
{
if (cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current())) {
if (!ri->IsDirectory())
SetRecording(ri->Recording()->FileName());
}
free(base);
}
void cMenuRecordings::SetHelpKeys(void)
{
cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
int NewHelpKeys = 0;
if (ri) {
if (ri->IsDirectory())
NewHelpKeys = 1;
else
NewHelpKeys = 2;
}
if (NewHelpKeys != helpKeys) {
switch (NewHelpKeys) {
case 0: SetHelp(NULL); break;
case 1: SetHelp(tr("Button$Open"), NULL, NULL, tr("Button$Edit")); break;
case 2: SetHelp(RecordingCommands.Count() ? tr("Commands") : tr("Button$Play"), tr("Button$Rewind"), tr("Button$Delete"), tr("Button$Info"));
default: ;
}
helpKeys = NewHelpKeys;
}
}
void cMenuRecordings::Set(bool Refresh)
{
if (cRecordings::GetRecordingsRead(recordingsStateKey)) {
recordingsStateKey.Remove();
const char *CurrentRecording = *fileName ? *fileName : cReplayControl::LastReplayed();
cRecordings *Recordings = cRecordings::GetRecordingsWrite(recordingsStateKey); // write access is necessary for sorting!
cMenuRecordingItem *LastItem = NULL;
if (Refresh) {
if (cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current()))
CurrentRecording = ri->Recording()->FileName();
}
Clear();
GetRecordingsSortMode(DirectoryName());
Recordings->Sort();
for (const cRecording *Recording = Recordings->First(); Recording; Recording = Recordings->Next(Recording)) {
if ((!filter || filter->Filter(Recording)) && (!base || (strstr(Recording->Name(), base) == Recording->Name() && Recording->Name()[strlen(base)] == FOLDERDELIMCHAR))) {
cMenuRecordingItem *Item = new cMenuRecordingItem(Recording, level);
cMenuRecordingItem *LastDir = NULL;
if (Item->IsDirectory()) {
// Sorting may ignore non-alphanumeric characters, so we need to explicitly handle directories in case they only differ in such characters:
for (cMenuRecordingItem *p = LastItem; p; p = dynamic_cast<cMenuRecordingItem *>(p->Prev())) {
if (p->Name() && strcmp(p->Name(), Item->Name()) == 0) {
LastDir = p;
break;
}
}
}
if (*Item->Text() && !LastDir) {
Add(Item);
LastItem = Item;
if (Item->IsDirectory())
LastDir = Item;
}
else
delete Item;
if (LastItem || LastDir) {
if (*path) {
if (strcmp(path, Recording->Folder()) == 0)
SetCurrent(LastDir ? LastDir : LastItem);
}
else if (CurrentRecording && strcmp(CurrentRecording, Recording->FileName()) == 0)
SetCurrent(LastDir ? LastDir : LastItem);
}
if (LastDir)
LastDir->IncrementCounter(Recording->IsNew());
}
}
SetMenuSortMode(RecordingsSortMode == rsmName ? msmName : msmTime);
recordingsStateKey.Remove(false); // sorting doesn't count as a real modification
}
if (Refresh)
Display();
}
void cMenuRecordings::SetPath(const char *Path)
{
path = Path;
}
void cMenuRecordings::SetRecording(const char *FileName)
{
fileName = FileName;
}
cString cMenuRecordings::DirectoryName(void)
{
cString d(cVideoDirectory::Name());
if (base) {
char *s = ExchangeChars(strdup(base), true);
d = AddDirectory(d, s);
free(s);
}
return d;
}
bool cMenuRecordings::Open(bool OpenSubMenus)
{
cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
if (ri && ri->IsDirectory() && (!*path || strcountchr(path, FOLDERDELIMCHAR) > 0)) {
const char *t = ri->Name();
cString buffer;
if (base) {
buffer = cString::sprintf("%s%c%s", base, FOLDERDELIMCHAR, t);
t = buffer;
}
AddSubMenu(new cMenuRecordings(t, level + 1, OpenSubMenus, filter));
return true;
}
return false;
}
eOSState cMenuRecordings::Play(void)
{
cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
if (ri) {
if (ri->IsDirectory())
Open();
else {
cReplayControl::SetRecording(ri->Recording()->FileName());
return osReplay;
}
}
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->Recording()->FileName(), ri->Recording()->IsPesRecording());
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?"))) {
if (cRecordControl *rc = cRecordControls::GetRecordControl(ri->Recording()->FileName())) {
if (Interface->Confirm(tr("Timer still recording - really delete?"))) {
if (cTimer *Timer = rc->Timer()) {
LOCK_TIMERS_WRITE;
Timer->Skip();
cRecordControls::Process(Timers, time(NULL));
if (Timer->IsSingleEvent()) {
Timers->Del(Timer);
isyslog("deleted timer %s", *Timer->ToDescr());
}
}
}
else
return osContinue;
}
cRecordings *Recordings = cRecordings::GetRecordingsWrite(recordingsStateKey);
Recordings->SetExplicitModify();
cRecording *Recording = Recordings->GetByName(ri->Recording()->FileName());
if (!Recording) {
Skins.Message(mtWarning, tr("Recording vanished!"));
recordingsStateKey.Remove();
return osContinue;
}
cString FileName = Recording->FileName();
if (RecordingsHandler.GetUsage(FileName)) {
if (Interface->Confirm(tr("Recording is being edited - really delete?"))) {
RecordingsHandler.Del(FileName);
Recording = Recordings->GetByName(FileName); // RecordingsHandler.Del() might have deleted it if it was the edited version
// we continue with the code below even if Recording is NULL,
// in order to have the menu updated etc.
}
else {
recordingsStateKey.Remove();
return osContinue;
}
}
if (cReplayControl::NowReplaying() && strcmp(cReplayControl::NowReplaying(), FileName) == 0)
cControl::Shutdown();
if (!Recording || Recording->Delete()) {
cReplayControl::ClearLastReplayed(FileName);
Recordings->DelByName(FileName);
cOsdMenu::Del(Current());
SetHelpKeys();
cVideoDiskUsage::ForceCheck();
Display();
Recordings->SetModified();
recordingsStateKey.Remove();
if (!Count())
return osBack;
return osUser2;
}
else
Skins.Message(mtError, tr("Error while deleting recording!"));
recordingsStateKey.Remove();
}
}
return osContinue;
}
eOSState cMenuRecordings::Info(void)
{
if (HasSubMenu() || Count() == 0)
return osContinue;
if (cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current())) {
if (ri->IsDirectory())
return AddSubMenu(new cMenuPathEdit(cString(ri->Recording()->Name(), strchrn(ri->Recording()->Name(), FOLDERDELIMCHAR, ri->Level() + 1))));
else
return AddSubMenu(new cMenuRecording(ri->Recording(), true));
}
return osContinue;
}
eOSState cMenuRecordings::Commands(eKeys Key)
{
if (HasSubMenu() || Count() == 0)
return osContinue;
cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current());
if (ri && !ri->IsDirectory()) {
cMenuCommands *menu;
eOSState state = AddSubMenu(menu = new cMenuCommands(tr("Recording commands"), &RecordingCommands, cString::sprintf("\"%s\"", *strescape(ri->Recording()->FileName(), "\\\"$"))));
if (Key != kNone)
state = menu->ProcessKey(Key);
return state;
}
return osContinue;
}
eOSState cMenuRecordings::Sort(void)
{
if (HasSubMenu())
return osContinue;
IncRecordingsSortMode(DirectoryName());
recordingsStateKey.Reset();
Set(true);
return osContinue;
}
eOSState cMenuRecordings::ProcessKey(eKeys Key)
{
if (!HasSubMenu())
Set(); // react on any changes to the recordings list
bool HadSubMenu = HasSubMenu();
eOSState state = cOsdMenu::ProcessKey(Key);
if (state == osUnknown) {
switch (Key) {
case kPlayPause:
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();
case k0: return Sort();
case k1...k9: return Commands(Key);
default: break;
}
}
else if (state == osUser1) {
// a recording or path was renamed, so let's refresh the menu
CloseSubMenu(false);
if (base)
return state; // closes all recording menus except for the top one
Set(); // this is the top level menu, so we refresh it...
Open(true); // ...and open any necessary submenus to show the new name
Display();
path = NULL;
fileName = NULL;
}
else if (state == osUser2) {
// a recording in a sub folder was deleted, so update the current item
cOsdMenu *m = HasSubMenu() ? SubMenu() : this;
if (cMenuRecordingItem *ri = (cMenuRecordingItem *)Get(Current())) {
if (cMenuRecordingItem *riSub = (cMenuRecordingItem *)m->Get(m->Current()))
ri->SetRecording(riSub->Recording());
}
}
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 (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;
cOsdProvider::UpdateOsdSize(true);
Setup.Save();
}
// --- cMenuSetupOSD ---------------------------------------------------------
class cMenuSetupOSD : public cMenuSetupBase {
private:
const char *useSmallFontTexts[3];
const char *recSortModeTexts[2];
const char *keyColorTexts[4];
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);
};
cMenuSetupOSD::cMenuSetupOSD(void)
{
SetMenuCategory(mcSetupOsd);
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");
recSortModeTexts[0] = tr("by name");
recSortModeTexts[1] = tr("by time");
keyColorTexts[0] = tr("Key$Red");
keyColorTexts[1] = tr("Key$Green");
keyColorTexts[2] = tr("Key$Yellow");
keyColorTexts[3] = tr("Key$Blue");
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()));
Add(new cMenuEditPrcItem( tr("Setup.OSD$Left (%)"), &data.OSDLeftP, 0.0, 0.5));
Add(new cMenuEditPrcItem( tr("Setup.OSD$Top (%)"), &data.OSDTopP, 0.0, 0.5));
Add(new cMenuEditPrcItem( tr("Setup.OSD$Width (%)"), &data.OSDWidthP, 0.5, 1.0));
Add(new cMenuEditPrcItem( tr("Setup.OSD$Height (%)"), &data.OSDHeightP, 0.5, 1.0));
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 cMenuEditPrcItem( tr("Setup.OSD$Default font size (%)"), &data.FontOsdSizeP, 0.01, 0.1, 1));
Add(new cMenuEditPrcItem( tr("Setup.OSD$Small font size (%)"), &data.FontSmlSizeP, 0.01, 0.1, 1));
Add(new cMenuEditPrcItem( tr("Setup.OSD$Fixed font size (%)"), &data.FontFixSizeP, 0.01, 0.1, 1));
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));
Add(new cMenuEditBoolItem(tr("Setup.OSD$Folders in timer menu"), &data.FoldersInTimerMenu));
Add(new cMenuEditBoolItem(tr("Setup.OSD$Always sort folders first"), &data.AlwaysSortFoldersFirst));
Add(new cMenuEditStraItem(tr("Setup.OSD$Default sort mode for recordings"), &data.DefaultSortModeRec, 2, recSortModeTexts));
Add(new cMenuEditBoolItem(tr("Setup.OSD$Number keys for characters"), &data.NumberKeysForChars));
Add(new cMenuEditStraItem(tr("Setup.OSD$Color key 0"), &data.ColorKey0, 4, keyColorTexts));
Add(new cMenuEditStraItem(tr("Setup.OSD$Color key 1"), &data.ColorKey1, 4, keyColorTexts));
Add(new cMenuEditStraItem(tr("Setup.OSD$Color key 2"), &data.ColorKey2, 4, keyColorTexts));
Add(new cMenuEditStraItem(tr("Setup.OSD$Color key 3"), &data.ColorKey3, 4, keyColorTexts));
SetCurrent(Get(current));
Display();
}
eOSState cMenuSetupOSD::ProcessKey(eKeys Key)
{
bool ModifiedAppearance = 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());
ModifiedAppearance = true;
}
}
if (themes.NumThemes() && Skins.Current()->Theme()) {
Skins.Current()->Theme()->Load(themes.FileName(themeIndex));
Utf8Strn0Cpy(data.OSDTheme, themes.Name(themeIndex), sizeof(data.OSDTheme));
ModifiedAppearance |= themeIndex != originalThemeIndex;
}
if (!(DoubleEqual(data.OSDLeftP, Setup.OSDLeftP) && DoubleEqual(data.OSDTopP, Setup.OSDTopP) && DoubleEqual(data.OSDWidthP, Setup.OSDWidthP) && DoubleEqual(data.OSDHeightP, Setup.OSDHeightP)))
ModifiedAppearance = true;
if (data.UseSmallFont != Setup.UseSmallFont || data.AntiAlias != Setup.AntiAlias)
ModifiedAppearance = 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) || !DoubleEqual(data.FontOsdSizeP, Setup.FontOsdSizeP))
ModifiedAppearance = true;
if (strcmp(data.FontSml, Setup.FontSml) || !DoubleEqual(data.FontSmlSizeP, Setup.FontSmlSizeP))
ModifiedAppearance = true;
if (strcmp(data.FontFix, Setup.FontFix) || !DoubleEqual(data.FontFixSizeP, Setup.FontFixSizeP))
ModifiedAppearance = true;
if (data.AlwaysSortFoldersFirst != Setup.AlwaysSortFoldersFirst || data.RecordingDirs != Setup.RecordingDirs) {
LOCK_RECORDINGS_WRITE;
Recordings->ClearSortNames();
}
}
int oldSkinIndex = skinIndex;
int oldOsdLanguageIndex = osdLanguageIndex;
eOSState state = cMenuSetupBase::ProcessKey(Key);
if (ModifiedAppearance) {
cOsdProvider::UpdateOsdSize(true);
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);
}
return state;
}
// --- cMenuSetupEPG ---------------------------------------------------------
class cMenuSetupEPG : public cMenuSetupBase {
private:
int originalNumLanguages;
int numLanguages;
void Setup(void);
public:
cMenuSetupEPG(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSetupEPG::cMenuSetupEPG(void)
{
SetMenuCategory(mcSetupEpg);
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();
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));
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)));
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];
const char *standardComplianceTexts[3];
public:
cMenuSetupDVB(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSetupDVB::cMenuSetupDVB(void)
{
SetMenuCategory(mcSetupDvb);
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");
standardComplianceTexts[0] = "DVB";
standardComplianceTexts[1] = "ANSI/SCTE";
standardComplianceTexts[2] = "NORDIG";
SetSection(tr("DVB"));
SetHelp(NULL, tr("Button$Audio"), tr("Button$Subtitles"), NULL);
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 cMenuEditStraItem(tr("Setup.DVB$Standard compliance"), &data.StandardCompliance, 3, standardComplianceTexts));
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));
}
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) {
switch (Key) {
case kGreen: cRemote::Put(kAudio, true);
state = osEnd;
break;
case kYellow: cRemote::Put(kSubtitles, true);
state = osEnd;
break;
default: {
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:
cSatCableNumbers satCableNumbers;
void Setup(void);
public:
cMenuSetupLNB(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSetupLNB::cMenuSetupLNB(void)
:satCableNumbers(MAXDEVICES)
{
SetMenuCategory(mcSetupLnb);
satCableNumbers.FromString(data.DeviceBondings);
SetSection(tr("LNB"));
Setup();
}
void cMenuSetupLNB::Setup(void)
{
int current = Current();
Clear();
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));
}
int NumSatDevices = 0;
for (int i = 0; i < cDevice::NumDevices(); i++) {
if (cDevice::GetDevice(i)->ProvidesSource(cSource::stSat))
NumSatDevices++;
}
if (NumSatDevices > 1) {
for (int i = 0; i < cDevice::NumDevices(); i++) {
if (cDevice::GetDevice(i)->ProvidesSource(cSource::stSat))
Add(new cMenuEditIntItem(cString::sprintf(tr("Setup.LNB$Device %d connected to sat cable"), i + 1), &satCableNumbers.Array()[i], 0, NumSatDevices, tr("Setup.LNB$own")));
else
satCableNumbers.Array()[i] = 0;
}
}
Add(new cMenuEditBoolItem(tr("Setup.LNB$Use dish positioner"), &data.UsePositioner));
if (data.UsePositioner) {
Add(new cMenuEditIntxItem(tr("Setup.LNB$Site latitude (degrees)"), &data.SiteLat, -900, 900, 10, tr("South"), tr("North")));
Add(new cMenuEditIntxItem(tr("Setup.LNB$Site longitude (degrees)"), &data.SiteLon, -1800, 1800, 10, tr("West"), tr("East")));
Add(new cMenuEditIntxItem(tr("Setup.LNB$Max. positioner swing (degrees)"), &data.PositionerSwing, 0, 900, 10));
Add(new cMenuEditIntxItem(tr("Setup.LNB$Positioner speed (degrees/s)"), &data.PositionerSpeed, 1, 1800, 10));
}
SetCurrent(Get(current));
Display();
}
eOSState cMenuSetupLNB::ProcessKey(eKeys Key)
{
int oldDiSEqC = data.DiSEqC;
int oldUsePositioner = data.UsePositioner;
bool DeviceBondingsChanged = false;
if (Key == kOk) {
cString NewDeviceBondings = satCableNumbers.ToString();
DeviceBondingsChanged = strcmp(data.DeviceBondings, NewDeviceBondings) != 0;
data.DeviceBondings = NewDeviceBondings;
}
eOSState state = cMenuSetupBase::ProcessKey(Key);
if (Key != kNone && (data.DiSEqC != oldDiSEqC || data.UsePositioner != oldUsePositioner))
Setup();
else if (DeviceBondingsChanged)
cDvbDevice::BondDevices(data.DeviceBondings);
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)
{
cString AssignedDevice("");
const char *Activating = "";
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;
}
}
else if (camSlot->IsActivating())
// TRANSLATORS: note the leading blank!
Activating = tr(" (activating)");
if (cDevice *Device = camSlot->Device())
AssignedDevice = cString::sprintf(" %s %d", tr("@ device"), Device->CardIndex() + 1);
cString buffer = cString::sprintf(" %d %s%s%s", camSlot->SlotNumber(), CamName, *AssignedDevice, Activating);
if (strcmp(buffer, Text()) != 0) {
SetText(buffer);
return true;
}
return false;
}
class cMenuSetupCAM : public cMenuSetupBase {
private:
const char *activationHelp;
eOSState Menu(void);
eOSState Reset(void);
eOSState Activate(void);
void SetHelpKeys(void);
public:
cMenuSetupCAM(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSetupCAM::cMenuSetupCAM(void)
{
activationHelp = NULL;
SetMenuCategory(mcSetupCam);
SetSection(tr("CAM"));
SetCols(15);
SetHasHotkeys();
for (cCamSlot *CamSlot = CamSlots.First(); CamSlot; CamSlot = CamSlots.Next(CamSlot))
Add(new cMenuSetupCAMItem(CamSlot));
SetHelpKeys();
}
void cMenuSetupCAM::SetHelpKeys(void)
{
if (HasSubMenu())
return;
cMenuSetupCAMItem *item = (cMenuSetupCAMItem *)Get(Current());
const char *NewActivationHelp = "";
if (item) {
cCamSlot *CamSlot = item->CamSlot();
if (CamSlot->IsActivating())
NewActivationHelp = tr("Button$Cancel activation");
else if (CamSlot->CanActivate())
NewActivationHelp = tr("Button$Activate");
}
if (NewActivationHelp != activationHelp) {
activationHelp = NewActivationHelp;
SetHelp(tr("Button$Menu"), tr("Button$Reset"), activationHelp);
}
}
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::Activate(void)
{
cMenuSetupCAMItem *item = (cMenuSetupCAMItem *)Get(Current());
if (item) {
cCamSlot *CamSlot = item->CamSlot();
if (CamSlot->IsActivating())
CamSlot->CancelActivation();
else if (CamSlot->CanActivate()) {
if (CamSlot->Priority() < LIVEPRIORITY) { // don't interrupt recordings
LOCK_CHANNELS_READ;
if (const cChannel *Channel = Channels->GetByNumber(cDevice::CurrentChannel())) {
for (int i = 0; i < cDevice::NumDevices(); i++) {
if (cDevice *Device = cDevice::GetDevice(i)) {
if (Device->ProvidesChannel(Channel)) {
if (Device->Priority() < LIVEPRIORITY) { // don't interrupt recordings
if (CamSlot->CanActivate()) {
if (CamSlot->Assign(Device, true)) { // query
cControl::Shutdown(); // must end transfer mode before assigning CAM, otherwise it might be unassigned again
if (CamSlot->Assign(Device)) {
if (Device->SwitchChannel(Channel, true)) {
CamSlot->StartActivation();
return osContinue;
}
}
}
}
}
}
}
}
}
}
Skins.Message(mtError, tr("Can't activate CAM!"));
}
}
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;
case kYellow: state = Activate(); break;
default: break;
}
for (cMenuSetupCAMItem *ci = (cMenuSetupCAMItem *)First(); ci; ci = (cMenuSetupCAMItem *)ci->Next()) {
if (ci->Changed())
DisplayItem(ci);
}
SetHelpKeys();
}
return state;
}
// --- cMenuSetupRecord ------------------------------------------------------
class cMenuSetupRecord : public cMenuSetupBase {
private:
const char *recordKeyHandlingTexts[3];
const char *pauseKeyHandlingTexts[3];
const char *delTimeshiftRecTexts[3];
public:
cMenuSetupRecord(void);
};
cMenuSetupRecord::cMenuSetupRecord(void)
{
SetMenuCategory(mcSetupRecord);
recordKeyHandlingTexts[0] = tr("no instant recording");
recordKeyHandlingTexts[1] = tr("confirm instant recording");
recordKeyHandlingTexts[2] = tr("record instantly");
pauseKeyHandlingTexts[0] = tr("do not pause live video");
pauseKeyHandlingTexts[1] = tr("confirm pause live video");
pauseKeyHandlingTexts[2] = tr("pause live video");
delTimeshiftRecTexts[0] = tr("no");
delTimeshiftRecTexts[1] = tr("confirm");
delTimeshiftRecTexts[2] = tr("yes");
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$Default priority"), &data.DefaultPriority, 0, MAXPRIORITY));
Add(new cMenuEditIntItem( tr("Setup.Recording$Default lifetime (d)"), &data.DefaultLifetime, 0, MAXLIFETIME));
Add(new cMenuEditStraItem(tr("Setup.Recording$Record key handling"), &data.RecordKeyHandling, 3, recordKeyHandlingTexts));
Add(new cMenuEditStraItem(tr("Setup.Recording$Pause key handling"), &data.PauseKeyHandling, 3, pauseKeyHandlingTexts));
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));
Add(new cMenuEditBoolItem(tr("Setup.Recording$Use episode name"), &data.UseSubtitle));
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, 0, MAXINSTANTRECTIME, tr("Setup.Recording$present event")));
Add(new cMenuEditIntItem( tr("Setup.Recording$Max. video file size (MB)"), &data.MaxVideoFileSize, MINVIDEOFILESIZE, MAXVIDEOFILESIZETS));
Add(new cMenuEditBoolItem(tr("Setup.Recording$Split edited files"), &data.SplitEditedFiles));
Add(new cMenuEditStraItem(tr("Setup.Recording$Delete timeshift recording"),&data.DelTimeshiftRec, 3, delTimeshiftRecTexts));
}
// --- cMenuSetupReplay ------------------------------------------------------
class cMenuSetupReplay : public cMenuSetupBase {
protected:
virtual void Store(void);
public:
cMenuSetupReplay(void);
};
cMenuSetupReplay::cMenuSetupReplay(void)
{
SetMenuCategory(mcSetupReplay);
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 cMenuEditBoolItem(tr("Setup.Replay$Show remaining time"), &data.ShowRemainingTime));
Add(new cMenuEditIntItem( tr("Setup.Replay$Progress display time (s)"), &data.ProgressDisplayTime, 0, 60));
Add(new cMenuEditBoolItem(tr("Setup.Replay$Pause replay when setting mark"), &data.PauseOnMarkSet));
Add(new cMenuEditBoolItem(tr("Setup.Replay$Pause replay when jumping to a mark"), &data.PauseOnMarkJump));
Add(new cMenuEditBoolItem(tr("Setup.Replay$Skip edited parts"), &data.SkipEdited));
Add(new cMenuEditBoolItem(tr("Setup.Replay$Pause replay at last mark"), &data.PauseAtLastMark));
Add(new cMenuEditIntItem( tr("Setup.Replay$Initial duration for adaptive skipping (s)"), &data.AdaptiveSkipInitial, 10, 600));
Add(new cMenuEditIntItem( tr("Setup.Replay$Reset timeout for adaptive skipping (s)"), &data.AdaptiveSkipTimeout, 0, 10));
Add(new cMenuEditBoolItem(tr("Setup.Replay$Alternate behavior for adaptive skipping"), &data.AdaptiveSkipAlternate));
Add(new cMenuEditBoolItem(tr("Setup.Replay$Use Prev/Next keys for adaptive skipping"), &data.AdaptiveSkipPrevNext));
Add(new cMenuEditIntItem( tr("Setup.Replay$Skip distance with Green/Yellow keys (s)"), &data.SkipSeconds, 5, 600));
Add(new cMenuEditIntItem( tr("Setup.Replay$Skip distance with Green/Yellow keys in repeat (s)"), &data.SkipSecondsRepeat, 5, 600));
Add(new cMenuEditIntItem(tr("Setup.Replay$Resume ID"), &data.ResumeID, 0, 99));
}
void cMenuSetupReplay::Store(void)
{
if (Setup.ResumeID != data.ResumeID) {
LOCK_RECORDINGS_WRITE;
Recordings->ResetResume();
}
cMenuSetupBase::Store();
}
// --- cMenuSetupMisc --------------------------------------------------------
class cMenuSetupMisc : public cMenuSetupBase {
private:
cStringList svdrpServerNames;
void Set(void);
public:
cMenuSetupMisc(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSetupMisc::cMenuSetupMisc(void)
{
SetMenuCategory(mcSetupMisc);
SetSection(tr("Miscellaneous"));
Set();
}
void cMenuSetupMisc::Set(void)
{
int current = Current();
Clear();
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 cMenuEditBoolItem(tr("Setup.Miscellaneous$SVDRP peering"), &data.SVDRPPeering));
if (data.SVDRPPeering) {
Add(new cMenuEditStrItem( tr("Setup.Miscellaneous$SVDRP host name"), data.SVDRPHostName, sizeof(data.SVDRPHostName)));
if (GetSVDRPServerNames(&svdrpServerNames)) {
svdrpServerNames.Sort(true);
svdrpServerNames.Insert(strdup(""));
Add(new cMenuEditStrlItem(tr("Setup.Miscellaneous$SVDRP default host"), data.SVDRPDefaultHost, sizeof(data.SVDRPDefaultHost), &svdrpServerNames));
}
}
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 cMenuEditIntItem( tr("Setup.Miscellaneous$Remote control repeat delay (ms)"), &data.RcRepeatDelay, 0));
Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Remote control repeat delta (ms)"), &data.RcRepeatDelta, 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")));
Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Volume steps"), &data.VolumeSteps, 5, 255));
Add(new cMenuEditIntItem( tr("Setup.Miscellaneous$Volume linearize"), &data.VolumeLinearize, -20, 20));
Add(new cMenuEditBoolItem(tr("Setup.Miscellaneous$Channels wrap"), &data.ChannelsWrap));
Add(new cMenuEditBoolItem(tr("Setup.Miscellaneous$Show channel names with source"), &data.ShowChannelNamesWithSource));
Add(new cMenuEditBoolItem(tr("Setup.Miscellaneous$Emergency exit"), &data.EmergencyExit));
SetCurrent(Get(current));
Display();
}
eOSState cMenuSetupMisc::ProcessKey(eKeys Key)
{
bool OldSVDRPPeering = data.SVDRPPeering;
bool ModifiedSVDRPSettings = false;
if (Key == kOk)
ModifiedSVDRPSettings = data.SVDRPPeering != Setup.SVDRPPeering | strcmp(data.SVDRPHostName, Setup.SVDRPHostName);
eOSState state = cMenuSetupBase::ProcessKey(Key);
if (data.SVDRPPeering != OldSVDRPPeering)
Set();
if (ModifiedSVDRPSettings) {
StopSVDRPClientHandler();
StopSVDRPServerHandler();
StartSVDRPServerHandler();
if (data.SVDRPPeering)
StartSVDRPClientHandler();
else {
LOCK_TIMERS_WRITE;
Timers->SetExplicitModify();
if (Timers->DelRemoteTimers())
Timers->SetModified();
}
}
return state;
}
// --- 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);
};
cMenuSetupPlugins::cMenuSetupPlugins(void)
{
SetMenuCategory(mcSetupPlugins);
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();
// Reinitialize OSD and skin, in case any plugin setup change has an influence on these:
cOsdProvider::UpdateOsdSize(true);
SetDisplayMenu();
Display();
}
}
return state;
}
// --- cMenuSetup ------------------------------------------------------------
class cMenuSetup : public cOsdMenu {
private:
virtual void Set(void);
eOSState Restart(void);
public:
cMenuSetup(void);
virtual eOSState ProcessKey(eKeys Key);
};
cMenuSetup::cMenuSetup(void)
:cOsdMenu("")
{
SetMenuCategory(mcSetup);
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));
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();
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, bool OpenSubMenus)
:cOsdMenu("")
{
SetMenuCategory(mcMain);
replaying = false;
stopReplayItem = NULL;
cancelEditingItem = NULL;
stopRecordingItem = NULL;
recordControlsState = 0;
Set();
// Initial submenus:
switch (State) {
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, OpenSubMenus)); break;
case osSetup: AddSubMenu(new cMenuSetup); break;
case osCommands: AddSubMenu(new cMenuCommands(tr("Commands"), &Commands)); break;
default: break;
}
}
cOsdObject *cMenuMain::PluginOsdObject(void)
{
cOsdObject *o = pluginOsdObject;
pluginOsdObject = NULL;
return o;
}
void cMenuMain::Set(void)
{
Clear();
SetTitle("VDR");
SetHasHotkeys();
// Basic menu items:
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));
// Plugins:
for (int i = 0; ; i++) {
cPlugin *p = cPluginManager::GetPlugin(i);
if (p) {
const char *item = p->MainMenuEntry();
if (item)
Add(new cMenuPluginItem(hk(item), i));
}
else
break;
}
// More basic menu items:
Add(new cOsdItem(hk(tr("Setup")), osSetup));
if (Commands.Count())
Add(new cOsdItem(hk(tr("Commands")), osCommands));
Update(true);
Display();
}
bool cMenuMain::Update(bool Force)
{
bool result = false;
bool NewReplaying = cControl::Control() != NULL;
if (Force || NewReplaying != replaying) {
replaying = NewReplaying;
// Replay control:
if (replaying && !stopReplayItem)
// TRANSLATORS: note the leading blank!
Add(stopReplayItem = new cOsdItem(tr(" Stop replaying"), osStopReplay));
else if (stopReplayItem && !replaying) {
Del(stopReplayItem->Index());
stopReplayItem = NULL;
}
// Color buttons:
SetHelp(!replaying && Setup.RecordKeyHandling ? tr("Button$Record") : NULL, tr("Button$Audio"), replaying || !Setup.PauseKeyHandling ? NULL : tr("Button$Pause"), replaying ? tr("Button$Stop") : cReplayControl::LastReplayed() ? tr("Button$Resume") : tr("Button$Play"));
result = true;
}
// Editing control:
bool EditingActive = RecordingsHandler.Active();
if (EditingActive && !cancelEditingItem) {
// TRANSLATORS: note the leading blank!
Add(cancelEditingItem = new cOsdItem(tr(" Cancel editing"), osCancelEdit));
result = true;
}
else if (cancelEditingItem && !EditingActive) {
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));
Add(item);
if (!stopRecordingItem)
stopRecordingItem = item;
}
result = true;
}
return result;
}
eOSState cMenuMain::ProcessKey(eKeys Key)
{
bool HadSubMenu = HasSubMenu();
int osdLanguage = I18nCurrentLanguage();
eOSState state = cOsdMenu::ProcessKey(Key);
HadSubMenu |= HasSubMenu();
switch (state) {
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));
case osStopRecord: if (Interface->Confirm(tr("Stop recording?"))) {
if (cOsdItem *item = Get(Current())) {
cRecordControls::Stop(item->Text() + strlen(tr(STOP_RECORDING)));
return osEnd;
}
}
break;
case osCancelEdit: if (Interface->Confirm(tr("Cancel editing?"))) {
RecordingsHandler.DelAll();
return osEnd;
}
break;
case osPlugin: {
cMenuPluginItem *item = (cMenuPluginItem *)Get(Current());
if (item) {
cPlugin *p = cPluginManager::GetPlugin(item->PluginIndex());
if (p) {
cOsdObject *menu = p->MainMenuAction();
if (menu) {
if (menu->IsMenu())
return AddSubMenu((cOsdMenu *)menu);
else {
pluginOsdObject = menu;
return osPlugin;
}
}
}
}
state = osEnd;
}
break;
default: switch (Key) {
case kRecord:
case kRed: if (!HadSubMenu)
state = replaying || !Setup.RecordKeyHandling ? osContinue : osRecord;
break;
case kGreen: if (!HadSubMenu) {
cRemote::Put(kAudio, true);
state = osEnd;
}
break;
case kYellow: if (!HadSubMenu)
state = replaying || !Setup.PauseKeyHandling ? osContinue : osPause;
break;
case kBlue: if (!HadSubMenu)
state = replaying ? osStopReplay : cReplayControl::LastReplayed() ? osReplay : osRecordings;
break;
default: break;
}
}
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;
if (LiveChannel) {
LOCK_CHANNELS_READ;
if (const cChannel *Channel = Channels->GetByNumber(LiveChannel)) {
LOCK_SCHEDULES_READ;
if (const cSchedule *Schedule = Schedules->GetSchedule(Channel)) {
const cEvent *Present = Schedule->GetPresentEvent();
if (Present)
Components = Present->Components();
}
}
}
else if (cReplayControl::NowReplaying()) {
LOCK_RECORDINGS_READ;
if (const cRecording *Recording = Recordings->GetByName(cReplayControl::NowReplaying()))
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;
case 4: cDevice::PrimaryDevice()->SetAvailableTrack(ttDolby, indexDolby++, 0, LiveChannel ? NULL : p->language, p->description);
break;
default: ;
}
}
}
}
// --- 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;
cOsdProvider::OsdSizeChanged(osdState); // just to get the current state
positioner = NULL;
channel = NULL;
LOCK_CHANNELS_READ;
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);
positioner = NULL;
channel = NULL;
LOCK_CHANNELS_READ;
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) {
LOCK_SCHEDULES_READ;
if (const cSchedule *Schedule = Schedules->GetSchedule(channel)) {
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);
}
const cChannel *cDisplayChannel::NextAvailableChannel(const cChannel *Channel, int Direction)
{
if (Direction) {
LOCK_CHANNELS_READ;
while (Channel) {
Channel = Direction > 0 ? Channels->Next(Channel) : Channels->Prev(Channel);
if (!Channel && Setup.ChannelsWrap)
Channel = Direction > 0 ? Channels->First() : Channels->Last();
if (Channel && !Channel->GroupSep() && cDevice::GetDevice(Channel, LIVEPRIORITY, true, true))
return Channel;
}
}
return NULL;
}
eOSState cDisplayChannel::ProcessKey(eKeys Key)
{
if (cOsdProvider::OsdSizeChanged(osdState)) {
delete displayChannel;
displayChannel = Skins.Current()->DisplayChannel(withInfo);
}
const cChannel *NewChannel = NULL;
if (Key != kNone)
lastTime.Set();
switch (int(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 > cChannels::MaxNumber())
number = Key - k0;
else
number = number * 10 + Key - k0;
LOCK_CHANNELS_READ
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;
const 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:
case kNext|k_Repeat:
case kNext:
case kPrev|k_Repeat:
case kPrev: {
withInfo = false;
number = 0;
LOCK_CHANNELS_READ;
if (group < 0) {
if (const cChannel *Channel = Channels->GetByNumber(cDevice::CurrentChannel()))
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);
if (const cChannel *Channel = NextAvailableChannel(channel, (k == kUp || k == kChanUp) ? 1 : -1))
channel = Channel;
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) {
LOCK_CHANNELS_READ;
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: {
LOCK_CHANNELS_READ;
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 (positioner || !timeout || lastTime.Elapsed() < (uint64_t)(Setup.ChannelInfoTime * 1000)) {
LOCK_CHANNELS_READ;
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();
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;
}
const cPositioner *Positioner = cDevice::ActualDevice()->Positioner();
bool PositionerMoving = Positioner && Positioner->IsMoving();
SetNeedsFastResponse(PositionerMoving);
if (!PositionerMoving) {
if (positioner)
lastTime.Set(); // to keep the channel display up a few seconds after the target position has been reached
Positioner = NULL;
}
if (Positioner || positioner) // making sure we call SetPositioner(NULL) if there is a switch from "with" to "without" positioner
displayChannel->SetPositioner(Positioner);
positioner = Positioner;
displayChannel->Flush();
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 (int(Key)) {
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 (int(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 (int(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, cTimers *Timers, cTimer *Timer, bool Pause)
{
// Whatever happens here, the timers will be modified in some way...
Timers->SetModified();
// We're going to work with an event here, so we need to prevent
// others from modifying any EPG data:
LOCK_SCHEDULES_READ;
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);
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);
if (!cReplayControl::LastReplayed()) // an instant recording, maybe from cRecordControls::PauseLiveVideo()
cReplayControl::SetRecording(fileName);
}
timer = NULL;
return;
}
cRecordingUserCommand::InvokeCommand(RUC_BEFORERECORDING, fileName);
isyslog("record %s", fileName);
if (MakeDirs(fileName, true)) {
const cChannel *ch = timer->Channel();
recorder = new cRecorder(fileName, ch, timer->Priority());
if (device->AttachReceiver(recorder)) {
Recording.WriteInfo();
cStatus::MsgRecording(device, Recording.Name(), Recording.FileName(), true);
if (!Timer && !cReplayControl::LastReplayed()) // an instant recording, maybe from cRecordControls::PauseLiveVideo()
cReplayControl::SetRecording(fileName);
LOCK_RECORDINGS_WRITE;
Recordings->AddByName(fileName);
return;
}
else
DELETENULL(recorder);
}
else
timer->SetDeferred(DEFERTIMER);
if (!Timer) {
Timers->Del(timer);
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++) {
{
LOCK_SCHEDULES_READ;
if (const cSchedule *Schedule = Schedules->GetSchedule(Channel)) {
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...");
cCondWait::SleepMs(1000);
}
dsyslog("no EPG info available");
return false;
}
void cRecordControl::Stop(bool ExecuteUserCommand)
{
if (timer) {
DELETENULL(recorder);
timer->SetRecording(false);
timer = NULL;
cStatus::MsgRecording(device, NULL, fileName, false);
if (ExecuteUserCommand)
cRecordingUserCommand::InvokeCommand(RUC_AFTERRECORDING, fileName);
}
}
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(cTimers *Timers, cTimer *Timer, bool Pause)
{
static time_t LastNoDiskSpaceMessage = 0;
int FreeMB = 0;
if (Timer) {
AssertFreeDiskSpace(Timer->Priority(), !Timer->Pending());
Timer->SetPending(true);
}
cVideoDirectory::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();
LOCK_CHANNELS_READ;
int ch = Timer ? Timer->Channel()->Number() : cDevice::CurrentChannel();
if (const cChannel *Channel = Channels->GetByNumber(ch)) {
int Priority = Timer ? Timer->Priority() : Pause ? Setup.PausePriority : Setup.DefaultPriority;
cDevice *device = cDevice::GetDevice(Channel, Priority, false);
if (device) {
dsyslog("switching device %d to channel %d (%s)", device->DeviceNumber() + 1, Channel->Number(), Channel->Name());
if (!device->SwitchChannel(Channel, false)) {
ShutdownHandler.RequestEmergencyExit();
return false;
}
if (!Timer || Timer->Matches()) {
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (!RecordControls[i]) {
RecordControls[i] = new cRecordControl(device, Timers, Timer, Pause);
return RecordControls[i]->Process(time(NULL));
}
}
}
}
else if (!Timer || !Timer->Pending()) {
isyslog("no free DVB device to record channel %d (%s)!", ch, Channel->Name());
Skins.Message(mtError, tr("No free DVB device to record!"));
}
}
else
esyslog("ERROR: channel %d not defined!", ch);
return false;
}
bool cRecordControls::Start(bool Pause)
{
LOCK_TIMERS_WRITE;
return Start(Timers, NULL, Pause);
}
void cRecordControls::Stop(const char *InstantId)
{
LOCK_TIMERS_WRITE;
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) {
Timers->Del(Timer);
isyslog("deleted timer %s", *Timer->ToDescr());
}
break;
}
}
}
}
void cRecordControls::Stop(cTimer *Timer)
{
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (RecordControls[i]) {
if (RecordControls[i]->Timer() == Timer) {
DELETENULL(RecordControls[i]);
ChangeState();
break;
}
}
}
}
bool cRecordControls::PauseLiveVideo(void)
{
Skins.Message(mtStatus, tr("Pausing live video..."));
cReplayControl::SetRecording(NULL); // make sure the new cRecordControl will set cReplayControl::LastReplayed()
if (Start(true)) {
cReplayControl *rc = new cReplayControl(true);
cControl::Launch(rc);
cControl::Attach();
Skins.Message(mtStatus, NULL);
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)
{
if (FileName) {
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (RecordControls[i] && strcmp(RecordControls[i]->FileName(), FileName) == 0)
return RecordControls[i];
}
}
return NULL;
}
cRecordControl *cRecordControls::GetRecordControl(const cTimer *Timer)
{
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (RecordControls[i] && RecordControls[i]->Timer() == Timer)
return RecordControls[i];
}
return NULL;
}
bool cRecordControls::Process(cTimers *Timers, time_t t)
{
bool Result = false;
for (int i = 0; i < MAXRECORDCONTROLS; i++) {
if (RecordControls[i]) {
if (!RecordControls[i]->Process(t)) {
DELETENULL(RecordControls[i]);
ChangeState();
Result = true;
}
}
}
return Result;
}
void cRecordControls::ChannelDataModified(const 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 (%s)", Channel->Number(), Channel->Name());
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;
}
// --- cAdaptiveSkipper ------------------------------------------------------
cAdaptiveSkipper::cAdaptiveSkipper(void)
{
initialValue = NULL;
currentValue = 0;
framesPerSecond = 0;
lastKey = kNone;
}
void cAdaptiveSkipper::Initialize(int *InitialValue, double FramesPerSecond)
{
initialValue = InitialValue;
framesPerSecond = FramesPerSecond;
currentValue = 0;
}
int cAdaptiveSkipper::GetValue(eKeys Key)
{
if (!initialValue)
return 0;
if (timeout.TimedOut()) {
currentValue = int(round(*initialValue * framesPerSecond));
lastKey = Key;
}
else if (Key != lastKey) {
currentValue /= 2;
if (Setup.AdaptiveSkipAlternate)
lastKey = Key; // only halve the value when the direction is changed
else
lastKey = kNone; // once the direction has changed, every further call halves the value
}
timeout.Set(Setup.AdaptiveSkipTimeout * 1000);
return max(currentValue, 1);
}
// --- cReplayControl --------------------------------------------------------
cReplayControl *cReplayControl::currentReplayControl = NULL;
cString cReplayControl::fileName;
cReplayControl::cReplayControl(bool PauseLive)
:cDvbPlayerControl(fileName, PauseLive)
{
cDevice::PrimaryDevice()->SetKeepTracks(PauseLive);
currentReplayControl = this;
displayReplay = NULL;
marksModified = false;
visible = modeOnly = shown = displayFrames = false;
lastCurrent = lastTotal = -1;
lastPlay = lastForward = false;
lastSpeed = -2; // an invalid value
timeoutShow = 0;
timeSearchActive = false;
cRecording Recording(fileName);
cStatus::MsgReplaying(this, Recording.Name(), Recording.FileName(), true);
marks.Load(fileName, Recording.FramesPerSecond(), Recording.IsPesRecording());
SetMarks(&marks);
adaptiveSkipper.Initialize(&Setup.AdaptiveSkipInitial, Recording.FramesPerSecond());
SetTrackDescriptions(false);
if (Setup.ProgressDisplayTime)
ShowTimed(Setup.ProgressDisplayTime);
}
cReplayControl::~cReplayControl()
{
cDevice::PrimaryDevice()->SetKeepTracks(false);
Hide();
cStatus::MsgReplaying(this, NULL, fileName, false);
Stop();
if (currentReplayControl == this)
currentReplayControl = NULL;
}
void cReplayControl::Stop(void)
{
if (Setup.DelTimeshiftRec && *fileName) {
cRecordControl* rc = cRecordControls::GetRecordControl(fileName);
if (rc && rc->InstantId()) {
if (Active()) {
if (Setup.DelTimeshiftRec == 2 || Interface->Confirm(tr("Delete timeshift recording?"))) {
{
LOCK_TIMERS_WRITE;
Timers->SetExplicitModify();
cTimer *Timer = rc->Timer();
rc->Stop(false); // don't execute user command
if (Timer) {
Timers->Del(Timer);
Timers->SetModified();
isyslog("deleted timer %s", *Timer->ToDescr());
}
}
cDvbPlayerControl::Stop();
LOCK_RECORDINGS_WRITE;
Recordings->SetExplicitModify();
if (cRecording *Recording = Recordings->GetByName(fileName)) {
if (Recording->Delete()) {
Recordings->DelByName(fileName);
ClearLastReplayed(fileName);
Recordings->SetModified();
}
else
Skins.Message(mtError, tr("Error while deleting recording!"));
}
return;
}
}
}
}
cDvbPlayerControl::Stop();
cMenuRecordings::SetRecording(NULL); // make sure opening the Recordings menu navigates to the last replayed recording
}
void cReplayControl::SetRecording(const char *FileName)
{
fileName = FileName;
}
const char *cReplayControl::NowReplaying(void)
{
return currentReplayControl ? *fileName : NULL;
}
const char *cReplayControl::LastReplayed(void)
{
LOCK_RECORDINGS_READ;
if (!Recordings->GetByName(fileName))
fileName = NULL;
return fileName;
}
void cReplayControl::ClearLastReplayed(const char *FileName)
{
if (*fileName && FileName && strcmp(fileName, FileName) == 0)
fileName = NULL;
}
void cReplayControl::ShowTimed(int Seconds)
{
if (modeOnly)
Hide();
if (!visible) {
shown = ShowProgress(true);
timeoutShow = (shown && Seconds > 0) ? time(NULL) + Seconds : 0;
}
else if (timeoutShow && Seconds > 0)
timeoutShow = time(NULL) + Seconds;
}
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;
timeoutShow = 0;
}
if (marksModified) {
marks.Save();
marksModified = 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 (*fileName) {
LOCK_RECORDINGS_READ;
if (const cRecording *Recording = Recordings->GetByName(fileName))
displayReplay->SetRecording(Recording);
}
lastCurrent = lastTotal = -1;
}
if (Current != lastCurrent || Total != lastTotal) {
if (Setup.ShowRemainingTime || Total != lastTotal) {
int Index = Total;
if (Setup.ShowRemainingTime)
Index = Current - Index;
displayReplay->SetTotal(IndexToHMSF(Index, false, FramesPerSecond()));
if (!Initial)
displayReplay->Flush();
}
displayReplay->SetProgress(Current, Total);
if (!Initial)
displayReplay->Flush();
displayReplay->SetCurrent(IndexToHMSF(Current, displayFrames, FramesPerSecond()));
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 = int(round(lastCurrent / FramesPerSecond()));
int Total = int(round(lastTotal / FramesPerSecond()));
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 kPlayPause:
case kPlay:
case kUp:
case kPause:
case kDown:
case kOk:
if (timeSearchPos > 0) {
Seconds = min(Total - STAY_SECONDS_OFF_END, Seconds);
bool Still = Key == kDown || Key == kPause || Key == kOk;
Goto(SecondsToFrames(Seconds, FramesPerSecond()), Still);
}
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)) {
lastCurrent = -1; // triggers redisplay
cStateKey StateKey;
marks.Lock(StateKey);
if (cMark *m = marks.Get(Current))
marks.Del(m);
else {
marks.Add(Current);
bool Play, Forward;
int Speed;
if (Setup.PauseOnMarkSet || GetReplayMode(Play, Forward, Speed) && !Play) {
Goto(Current, true);
displayFrames = true;
}
}
StateKey.Remove();
ShowTimed(2);
marksModified = true;
}
}
void cReplayControl::MarkJump(bool Forward)
{
int Current, Total;
if (GetIndex(Current, Total)) {
if (marks.Count()) {
if (cMark *m = Forward ? marks.GetNext(Current) : marks.GetPrev(Current)) {
if (!Setup.PauseOnMarkJump) {
bool Playing, Fwd;
int Speed;
if (GetReplayMode(Playing, Fwd, Speed) && Playing && Forward && m->Position() < Total - SecondsToFrames(3, FramesPerSecond())) {
Goto(m->Position());
return;
}
}
Goto(m->Position(), true);
displayFrames = true;
return;
}
}
// There are either no marks at all, or we already were at the first or last one,
// so jump to the very beginning or end:
Goto(Forward ? Total : 0, true);
}
}
void cReplayControl::MarkMove(int Frames, bool MarkRequired)
{
int Current, Total;
if (GetIndex(Current, Total)) {
bool Play, Forward;
int Speed;
GetReplayMode(Play, Forward, Speed);
cMark *m = marks.Get(Current);
if (!Play && m) {
displayFrames = true;
cMark *m2;
if (Frames > 0) {
// Handle marks at the same offset:
while ((m2 = marks.Next(m)) != NULL && m2->Position() == m->Position())
m = m2;
// Don't skip the next mark:
if ((m2 = marks.Next(m)) != NULL)
Frames = min(Frames, m2->Position() - m->Position() - 1);
}
else {
// Handle marks at the same offset:
while ((m2 = marks.Prev(m)) != NULL && m2->Position() == m->Position())
m = m2;
// Don't skip the next mark:
if ((m2 = marks.Prev(m)) != NULL)
Frames = -min(-Frames, m->Position() - m2->Position() - 1);
}
int p = SkipFrames(Frames);
m->SetPosition(p);
Goto(m->Position(), true);
marksModified = true;
}
else if (!MarkRequired)
Goto(SkipFrames(Frames), !Play);
}
}
void cReplayControl::EditCut(void)
{
if (*fileName) {
Hide();
if (!RecordingsHandler.GetUsage(fileName)) {
if (!marks.Count())
Skins.Message(mtError, tr("No editing marks defined!"));
else if (!marks.GetNumSequences())
Skins.Message(mtError, tr("No editing sequences defined!"));
else if (access(cCutter::EditedFileName(fileName), F_OK) == 0 && !Interface->Confirm(tr("Edited version already exists - overwrite?")))
;
else if (!RecordingsHandler.Add(ruCut, 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) {
if ((m->Index() & 0x01) != 0 && !Setup.SkipEdited) // when skipping edited parts we also need to jump to end marks
m = marks.Next(m);
if (m)
Goto(m->Position() - SecondsToFrames(3, FramesPerSecond()));
}
}
}
cOsdObject *cReplayControl::GetInfo(void)
{
LOCK_RECORDINGS_READ;
if (const cRecording *Recording = Recordings->GetByName(cReplayControl::LastReplayed()))
return new cMenuRecording(Recording, false);
return NULL;
}
const cRecording *cReplayControl::GetRecording(void)
{
LOCK_RECORDINGS_READ;
if (const cRecording *Recording = Recordings->GetByName(LastReplayed()))
return Recording;
return NULL;
}
eOSState cReplayControl::ProcessKey(eKeys Key)
{
if (!Active())
return osEnd;
if (Key == kNone && !marksModified)
marks.Update();
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;
}
if (Key == kPlayPause) {
bool Play, Forward;
int Speed;
GetReplayMode(Play, Forward, Speed);
if (Speed >= 0)
Key = Play ? kPlay : kPause;
else
Key = Play ? kPause : kPlay;
}
bool DoShowMode = true;
switch (int(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;
case kGreen|k_Repeat:
SkipSeconds(-Setup.SkipSecondsRepeat); break;
case kGreen: SkipSeconds(-Setup.SkipSeconds); break;
case kYellow|k_Repeat:
SkipSeconds(Setup.SkipSecondsRepeat); break;
case kYellow: SkipSeconds(Setup.SkipSeconds); break;
case kStop:
case kBlue: Hide();
Stop();
return osEnd;
default: {
DoShowMode = false;
switch (int(Key)) {
// Editing:
case kMarkToggle: MarkToggle(); break;
case kPrev|k_Repeat:
case kPrev: if (Setup.AdaptiveSkipPrevNext) {
MarkMove(-adaptiveSkipper.GetValue(RAWKEY(Key)), false);
break;
}
// fall through...
case kMarkJumpBack|k_Repeat:
case kMarkJumpBack: MarkJump(false); break;
case kNext|k_Repeat:
case kNext: if (Setup.AdaptiveSkipPrevNext) {
MarkMove(+adaptiveSkipper.GetValue(RAWKEY(Key)), false);
break;
}
// fall through...
case kMarkJumpForward|k_Repeat:
case kMarkJumpForward: MarkJump(true); break;
case kMarkMoveBack|k_Repeat:
case kMarkMoveBack: MarkMove(-1, true); break;
case kMarkMoveForward|k_Repeat:
case kMarkMoveForward: MarkMove(+1, true); break;
case kMarkSkipBack|k_Repeat:
case kMarkSkipBack: MarkMove(-adaptiveSkipper.GetValue(RAWKEY(Key)), false); break;
case kMarkSkipForward|k_Repeat:
case kMarkSkipForward: MarkMove(+adaptiveSkipper.GetValue(RAWKEY(Key)), false); 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;
case kBack: Hide();
Stop();
return osRecordings;
default: return osUnknown;
}
}
}
}
}
if (DoShowMode)
ShowMode();
return osContinue;
}