2 Copyright 2005 Brian Walton
4 This file is part of VOMP.
6 VOMP is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 VOMP is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with VOMP; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 vepg presents a 2 dimensional electronic programme guide with channels down
22 the y axis and time along the x axis.
23 Programmes are layed on the x axis as alterate coloured blocks with as much
24 of the programme title as will fit inside the block shown as text.
25 Up and down commands step through the channels whilst left and right commands
26 move through the programmes of the currently selected channel.
27 When a programme is selected, it highlights in the grid and full programe details
28 (start time, title and description) are displayed in an area at te top left of the screen.
29 Any currently programmed timers will display in the grid and in the orogramme detail window as red
30 It is possible to select a programme to be recorded by pressing the record button.
31 The video stream currently being viewed is shown as quarter screen in the top right.
36 VEpg* VEpg::instance = NULL;
38 VEpg::VEpg(VVideoLive* v, UINT currentChannel, ULONG streamType)
42 // PAL / NTSC sizes -----------------------
46 int summaryLines, summaryLowerPadding;
48 //UINT gridRows; // is a class member
50 if (Video::getInstance()->getFormat() == Video::PAL)
57 summaryLowerPadding = 16;
68 summaryLowerPadding = 26;
73 // initialise variables and pointers
74 viewman = ViewMan::getInstance();
77 chanList = VDR::getInstance()->getChannelsList(streamType); //TODO want to be able to display video and radio together
80 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
82 // initialise array of pointers to eventlist structures
83 eventLista[listIndex] = NULL;
86 // Create pallet on which to paint our epg view and position it in centre of screen.
87 // Need to reduce size to deal with overscanning TVs.
90 setScreenPos(xpos, ypos);
93 Colour transparent = Colour(0, 0, 0, 0);
94 setBackgroundColour(transparent);
96 progTitle.setSurface(surface);
97 progTitle.setSurfaceOffset(0,0);
98 progTitle.setDimensions(300,(Surface::getFontHeight() + 4) * 2 + 16); //paragraph line seperation is 4 pixels
99 progTitle.setBackgroundColour(Colour::TITLEBARBACKGROUND);
100 progTitle.setTextPos(5, 16);
103 progInfo.setSurface(surface);
104 progInfo.setSurfaceOffset(0, progTitle.getOffsetY() + progTitle.getHeight());
105 progInfo.setDimensions(300,((Surface::getFontHeight() + 4) * summaryLines) + summaryLowerPadding);
108 chanName.setSurface(surface);
109 chanName.setDimensions(510, (Surface::getFontHeight() + 4));
110 chanName.setSurfaceOffset(305, chanNameYpos);
111 chanName.setBackgroundColour(Colour(0, 0, 0, 90));
113 // create area to display list of channels
114 chanListbox.setSurface(surface); // add channel list
115 chanListbox.setSurfaceOffset(0, progInfo.getOffsetY() + progInfo.getHeight() + Surface::getFontHeight() + 8); // position channel list
116 chanListbox.setDimensions(150, ((Surface::getFontHeight() + 2) * gridRows) + 5); //listbox line seperation is 2 pixels
117 chanListbox.setGap(2);
120 // populate channel list
125 for (UINT i = 0; i < chanList->size(); i++)
127 chan = (*chanList)[i];
128 if (i == currentChannel)
130 chan->index = chanListbox.addOption(chan->name, 0, first);
133 chanName.setText((*chanList)[chanListbox.getCurrentOption()]->name);
136 listTop = chanListbox.getTopOption();
137 chanListbox.draw(); // doing this to allow chanListbox.getBottomOption() in updateEventList() to work
138 time(<ime); // set ltime to now
139 ltime = prevHour(<ime); // set ltime to previous hour TODO make this half hour?
140 time(&selTime); // set selTime to now
141 updateEventList(); // get list of programmes
146 Timers::getInstance()->cancelTimer(this, 1);
150 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
152 if (eventLista[listIndex])
154 (eventLista)[listIndex]->clear();
155 delete eventLista[listIndex];
158 // delete [] eventLista;
160 // destroy dynamically allocated memory
163 VEpg* VEpg::getInstance()
168 void VEpg::setInfo(Event* event)
171 struct tm* btime; // to hold programme start and end time
172 char timeString[9]; // to hold programme start and end time
173 int length = strlen(event->title); // calculate length of programme title string
174 char* title = new char[length + 15]; // create string to hold start time, end time and programme title
175 btime = localtime((time_t*)&event->time); //get programme start time
177 strftime(timeString, 9, "%0H:%0M - ", btime); // and format it as hh:mm -
179 strftime(timeString, 9, "%H:%M - ", btime); // and format it as hh:mm -
181 strcpy(title, timeString); // put it in our buffer
182 t = event->time + event->duration; //get programme end time
183 btime = localtime(&t);
185 strftime(timeString, 7, "%0H:%0M ", btime); // and format it as hh:mm -
187 strftime(timeString, 7, "%H:%M ", btime); // and format it as hh:mm -
189 strcat(title, timeString); // put it in our buffer
190 strcat(title, event->title); // then add the programme title
191 progTitle.setText(title); // sput this sring in our text box
192 length = strlen(event->description);
193 char* info = new char[length + 1]; // create programme detail string
194 strcpy(info, event->description);
195 progInfo.setText(info); // show programme detail string
196 // destroy dynamically allocated memory
203 View::draw(); // draw pallet
205 // Moved all the dynamic data drawing to a seperate function
207 // Display the status and key stuff at the bottom
208 int keyx = chanListbox.getOffsetX();
209 int keyy = chanListbox.getOffsetY() + chanListbox.getHeight() + 2;
210 rectangle(keyx, keyy, 605, Surface::getFontHeight() * 2 + 14, Colour(100, 100, 100, 255));
213 w.setSurface(surface);
215 w.nextSymbol = WSymbol::LEFTARROW;
216 w.setSurfaceOffset(keyx + 1, keyy + 20);
219 w.nextSymbol = WSymbol::UP;
220 w.setSurfaceOffset(keyx + 26, keyy + 3);
223 w.nextSymbol = WSymbol::DOWN;
224 w.setSurfaceOffset(keyx + 26, keyy + 36);
227 w.nextSymbol = WSymbol::RIGHTARROW;
228 w.setSurfaceOffset(keyx + 50, keyy + 20);
231 drawText(tr("OK"), keyx + 18, keyy + 20, Colour::LIGHTTEXT);
233 rectangle(keyx + 72, keyy + 4, 104, Surface::getFontHeight() + 2, Colour(200, 0, 0, 255));
234 drawText(tr("Page up"), keyx + 74, keyy + 5, Colour::LIGHTTEXT);
236 rectangle(keyx + 72, keyy + Surface::getFontHeight() + 8, 104, Surface::getFontHeight() + 2, Colour(0, 200, 0, 255));
237 drawText(tr("Page down"), keyx + 74, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
239 rectangle(keyx + 180, keyy + 4, 104, Surface::getFontHeight() + 2, Colour(200, 200, 0, 255));
240 drawText(tr("-24 hours"), keyx + 182, keyy + 5, Colour::LIGHTTEXT);
242 rectangle(keyx + 180, keyy + Surface::getFontHeight() + 8, 104, Surface::getFontHeight() + 2, Colour(0, 0, 200, 255));
243 drawText(tr("+24 hours"), keyx + 182, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
245 rectangle(keyx + 290, keyy + 4, 180, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
246 drawText(tr("Guide / Back: Close"), keyx + 292 , keyy + 5, Colour::LIGHTTEXT);
248 rectangle(keyx + 290, keyy + Surface::getFontHeight() + 8, 180, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
249 Colour red = Colour(130, 0, 0);
250 drawText(tr("Rec: Set timer"), keyx + 292, keyy + Surface::getFontHeight() + 9, red);
252 rectangle(keyx + 474, keyy + 4, 128, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
253 w.nextSymbol = WSymbol::PLAY;
254 w.setSurfaceOffset(keyx + 476, keyy + 5);
256 drawText(tr("Sel channel"), keyx + 496, keyy + 5, Colour::LIGHTTEXT);
258 rectangle(keyx + 474, keyy + Surface::getFontHeight() + 8, 128, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
259 drawText(tr("Go: Preview"), keyx + 476, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
261 // Draw all the dynamic data
265 void VEpg::drawData()
267 // Not doing View::draw() every time causes
268 // things not to be cleared off the surface properly
269 // So, blank out the data area first
272 chanListbox.getOffsetX(),
273 chanListbox.getOffsetY() - Surface::getFontHeight() - 3,
274 155 + WINDOW_WIDTH * MINUTE_SCALE,
275 chanListbox.getHeight() + Surface::getFontHeight() + 4,
280 chanName.draw(); // TODO this should be dealt with by vvideolive
284 // Set timer to redraw to move the current time bar
288 if (dt == 0) dt = 60;
290 Timers::getInstance()->setTimerT(this, 1, dt);
293 void VEpg::timercall(int clientReference)
296 // Put updateView through master mutex since viewman is not mutex protected
297 Message* m = new Message();
298 m->message = Message::REDRAW;
299 m->to = ViewMan::getInstance();
302 Command::getInstance()->postMessageFromOuterSpace(m);
305 int VEpg::handleCommand(int command)
311 { // cursor up the channel list
314 viewman->updateView(this);
317 case Remote::DF_DOWN:
319 { // cursor down the channel list
322 viewman->updateView(this);
325 case Remote::DF_LEFT:
327 { // cursor left through time
328 selTime = thisEvent.time - 1;
330 viewman->updateView(this);
333 case Remote::DF_RIGHT:
336 // cursor right through time
337 selTime = thisEvent.time + thisEvent.duration;
339 viewman->updateView(this);
344 // cursor up one page
345 chanListbox.pageUp();
347 viewman->updateView(this);
352 // cursor down one page
353 chanListbox.pageDown();
355 viewman->updateView(this);
360 // step forward 24 hours
361 selTime += 24 * 60 * 60;
363 viewman->updateView(this);
368 // step forward 24 hours
369 selTime -= 24 * 60 * 60;
371 viewman->updateView(this);
376 if (!chanList) return 2;
377 Log::getInstance()->log("VEPG", Log::DEBUG, "ID %lu TIME %lu DURATION %lu TITLE %s", thisEvent.id, thisEvent.time, thisEvent.duration, thisEvent.title);
378 VEpgSetTimer* vs = new VEpgSetTimer(&thisEvent, (*chanList)[chanListbox.getCurrentOption()]);
381 viewman->updateView(vs);
388 if (!chanList) return 2;
390 // select programme and display menu TODO currently just changes to selected channel
391 videoLive->channelChange(VVideoLive::NUMBER, (*chanList)[chanListbox.getCurrentOption()]->number);
393 if(command == Remote::GO)
395 // GO just changes channel in preview, PLAY changes channel and returns to normal TV
400 // return to normal TV mode
401 if (videoLive) // ptr check done in case being tested from videorec
403 Message* m = new Message(); // Must be done after this view deleted
406 m->message = Message::EPG_CLOSE;
407 Command::getInstance()->postMessageNoLock(m);
411 case Remote::CHANNELUP:
413 videoLive->channelChange(VVideoLive::OFFSET, VVideoLive::UP);
416 case Remote::CHANNELDOWN:
418 videoLive->channelChange(VVideoLive::OFFSET, VVideoLive::DOWN);
422 // stop command getting to any more views
426 void VEpg::drawgrid() // redraws grid and select programme
428 // draw the grid of programmes
431 time(&t); // set t = now
433 selTime = t; // don't allow cursor in the past
434 if(listTop != chanListbox.getTopOption())
436 // chanListbox has scrolled TODO speed up by changing only rows that have changed
437 listTop = chanListbox.getTopOption();
440 if ((selTime >= ltime + WINDOW_WIDTH * 60) || (selTime <= ltime))
442 // we have cursored back before left time of window
443 //TODO check that this and above don't happen together
444 ltime = prevHour(&selTime);
451 strftime(timeString, 19, "%a %e %b", tms);
452 int timey = chanListbox.getOffsetY() - Surface::getFontHeight() - 3;
454 drawTextRJ(timeString, timex - 10, timey, Colour::LIGHTTEXT); // print date
455 strftime(timeString, 19, "%H:%M", tms);
456 drawText(timeString, timex, timey, Colour::LIGHTTEXT); // print left time
457 rectangle(155, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
460 strftime(timeString, 19, "%H:%M", tms);
461 drawText(timeString, timex + 180, timey, Colour::LIGHTTEXT); // print middle time
462 rectangle(335, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
465 strftime(timeString, 19, "%H:%M", tms);
466 drawText(timeString, timex + 360, timey, Colour::LIGHTTEXT); // print right time
467 rectangle(515, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
468 // pointer to selTime
469 //rectangle(155 + (selTime - ltime) / 20, timey + Surface::getFontHeight(), 2, 7, Colour(255, 50, 50, 255));
473 if ((t >= ltime) && (t < (ltime + 9000)))
475 rectangle(155 + (t - ltime) / 20, timey + Surface::getFontHeight(), 2, ((Surface::getFontHeight() + 2) * 7) + 7 + 2, Colour::RED);
478 // TODO should the above two comditional statements be combined to avoid calling updateEventList() twice?
480 Event noevent; // an event to use if there are gaps in the epg
481 thisEvent.setdescription(tr("There are no programme details available for this period"));
482 thisEvent.duration = WINDOW_WIDTH * 60;
483 thisEvent.time = ltime;
484 thisEvent.settitle(tr("No programme details"));
486 bool swapColour = FALSE; // alternate cell colour
487 bool currentRow = FALSE;
488 int y = chanListbox.getOffsetY() + 5; // vertical position of cell
489 Colour bg, fg; // background colour of cells in grid
490 // for each displayed channel, find programmes that fall in 2.5 hour time window
491 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
493 if (listTop + (int)listIndex >= chanListbox.getBottomOption())
494 continue; // ensure nothing populates grid below last channel
495 currentRow = (listTop + (int)listIndex == chanListbox.getCurrentOption());
496 noevent.time = ltime;
497 noevent.duration = WINDOW_WIDTH * 60;
498 noevent.settitle("");
499 paintCell(&noevent, y, Colour::NOPROGRAMME, Colour::LIGHTTEXT); // fill row with no programme colour to be painted ove with valid programmes
502 thisEvent.setdescription(tr("There are no programme details available for this period"));
503 thisEvent.duration = WINDOW_WIDTH * 60;
504 thisEvent.time = ltime;
505 thisEvent.settitle(tr("No programme details"));
508 if (eventLista[listIndex])
510 sort(eventLista[listIndex]->begin(), eventLista[listIndex]->end(), EventSorter());
511 for(e = 0; e < (eventLista[listIndex])->size(); e++) // step through events for this channel
513 fg = Colour::LIGHTTEXT;
514 event = (*eventLista[listIndex])[e];
517 UINT end = event->time + event->duration; // programme end time
518 if(event->time >= UINT(ltime) + (WINDOW_WIDTH * 60)) // programme starts after RHS of window
519 continue; // that's enough of this channel's events
520 if(end <= UINT(ltime)) // programme ends before LHS of window
521 continue; // this event is before the window - let's try the next event
522 // this event is one we are interested in
523 bg = (swapColour)?Colour::PROGRAMMEA:Colour::PROGRAMMEB; // alternate cell colour
524 swapColour = !swapColour; // it wil be the other colour next time
525 if(event->time <= UINT(selTime) && end > UINT(selTime) && currentRow)
527 // this is the selected programme
528 thisEvent.setdescription(event->description);
529 thisEvent.duration = event->duration;
530 thisEvent.time = event->time;
531 thisEvent.settitle(event->title);
532 thisEvent.id = event->id;
533 if(thisEvent.id == 0)
535 bg = Colour::SELECTHIGHLIGHT; // highlight cell
536 fg = Colour::DARKTEXT;
540 if (currentRow && thisEvent.id == 0)
542 if (end <= UINT(selTime) && end > UINT(thisEvent.time))
543 thisEvent.time = end;
544 if (event->time > UINT(selTime) && event->time < thisEvent.time + thisEvent.duration)
545 thisEvent.duration = event->time - thisEvent.time;
548 paintCell(event, y, bg, fg);
554 // no event list for this channel. Already painted noevent colour so just highlight if selected
557 bg = Colour::SELECTHIGHLIGHT; // highlight cell
558 fg = Colour::DARKTEXT;
559 paintCell(&thisEvent, y, bg, fg);
563 bg = Colour::NOPROGRAMME;
564 fg = Colour::LIGHTTEXT;
565 noevent.settitle(tr("No programme details"));
566 paintCell(&noevent, y, bg, fg);
569 y += Surface::getFontHeight() + 2;
574 void VEpg::updateEventList()
576 if (!chanList) return;
578 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
580 if(listTop + listIndex >= UINT(chanListbox.getBottomOption()))
582 chan = (*chanList)[listTop + listIndex];
584 eventLista[listIndex] = VDR::getInstance()->getChannelSchedule(chan->number, ltime - 1, WINDOW_WIDTH * 60 + 2); // ltime - 1 to get prog before window (allows cursor left past ltime). + 2 to get prog after window
588 void VEpg::setCurrentChannel(char* chname)
590 chanName.setText(chname);
592 viewman->updateView(this);
595 void VEpg::paintCell(Event* event, int yOffset, Colour bg, Colour fg)
598 w = x = 0; // keep compiler happy
601 h = Surface::getFontHeight(); // TODO if want border around text, need to increae this and wselectlist line height
602 UINT end = event->time + event->duration; // programme end time
603 if(event->time <= UINT(ltime) && end > UINT(ltime)) // spans start of displayed window
605 x = 155; // LHS of window
606 if (end > (UINT(ltime) + (WINDOW_WIDTH * 60)))
607 w = WINDOW_WIDTH * MINUTE_SCALE; // spans full 2 hour window
609 w = MINUTE_SCALE * (event->time + event->duration - ltime ) / 60; // get width of remaining programme
611 if((event->time >= UINT(ltime)) && (event->time <= UINT(ltime) + (WINDOW_WIDTH * 60))) // starts within window
613 x = 155 + (MINUTE_SCALE * (event->time - ltime) / 60);
614 w = MINUTE_SCALE * event->duration / 60;
615 //if (w > 155 + MINUTE_SCALE * WINDOW_WIDTH -x)
616 // w = w + x - 155 - MINUTE_SCALE * WINDOW_WIDTH; // ends outside window
618 if (w > 155 + WINDOW_WIDTH * MINUTE_SCALE - x)
619 w = 155 + WINDOW_WIDTH * MINUTE_SCALE -x; // limit cells to RHS of window
620 rectangle(x, y, w, h, bg);
621 char* tt = new char[strlen(event->title) + 1];
622 strcpy (tt, event->title);
624 for (UINT textPos = 0; textPos < strlen(tt); textPos++)
626 int thisCharWidth = surface->getCharWidth(tt[textPos]);
627 if (textWidth + thisCharWidth > w) // text will not fit in cell
632 textWidth += thisCharWidth;
634 char* tT = new char[textWidth];
637 strncpy(tT, tt, textWidth - 1);
638 tT[textWidth - 1] = '\0';
639 surface->drawText(tT, x+2, y, fg.rgba());
645 time_t VEpg::prevHour(time_t* t)
654 void VEpg::processMessage(Message* m)
656 if (m->message == Message::MOUSE_MOVE)
658 if (chanListbox.mouseMove((m->parameter>>16)-getScreenX(),(m->parameter&0xFFFF)-getScreenY()))
661 ViewMan::getInstance()->updateView(this);
664 else if (m->message == Message::MOUSE_LBDOWN)
666 if (chanListbox.mouseLBDOWN((m->parameter>>16)-getScreenX(),(m->parameter&0xFFFF)-getScreenY()))
668 ViewMan::getInstance()->handleCommand(Remote::OK); //simulate OK press
672 //check if press is outside this view! then simulate cancel
673 int x=(m->parameter>>16)-getScreenX();
674 int y=(m->parameter&0xFFFF)-getScreenY();
675 int keyx = chanListbox.getOffsetX();
676 int keyy = chanListbox.getOffsetY() + chanListbox.getHeight() + 2;
678 if (x<0 || y <0 || x>getWidth() || y>getHeight())
680 ViewMan::getInstance()->handleCommand(Remote::BACK); //simulate cancel press
682 else if (x>=(keyx+72) && y>=(keyy+4) &&x<=(keyx+72+104) &&y<=(keyy+4+Surface::getFontHeight() + 2))
684 ViewMan::getInstance()->handleCommand(Remote::RED);
686 else if (x>=(keyx+72) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+72+104) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
688 ViewMan::getInstance()->handleCommand(Remote::GREEN);
690 else if (x>=(keyx+180) && y>=(keyy+4) &&x<=(keyx+180+104) &&y<=(keyy+4+Surface::getFontHeight() + 2))
692 ViewMan::getInstance()->handleCommand(Remote::YELLOW);
694 else if (x>=(keyx+180) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+180+104) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
696 ViewMan::getInstance()->handleCommand(Remote::BLUE);
698 else if (x>=(keyx+290) && y>=(keyy+4) &&x<=(keyx+180+290) &&y<=(keyy+4+Surface::getFontHeight() + 2))
700 ViewMan::getInstance()->handleCommand(Remote::BACK);
702 else if (x>=(keyx+290) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+290+180) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
704 ViewMan::getInstance()->handleCommand(Remote::RECORD);
706 else if (x>=(keyx+474) && y>=(keyy+4) &&x<=(keyx+128+474) &&y<=(keyy+4+Surface::getFontHeight() + 2))
708 ViewMan::getInstance()->handleCommand(Remote::PLAY);
710 else if (x>=(keyx+474) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+238+474) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
712 ViewMan::getInstance()->handleCommand(Remote::GO);
714 else if ( x>=(chanListbox.getOffsetX())
715 && y>=(chanListbox.getOffsetY() + 5)
716 // &&x<=(chanListbox.getOffsetX()+155 + WINDOW_WIDTH * MINUTE_SCALE)
717 &&y<=(chanListbox.getOffsetY() - Surface::getFontHeight()
718 - 3+chanListbox.getHeight() + Surface::getFontHeight() + 3)
721 int cy=y-(chanListbox.getOffsetY() + 5);
722 int row=cy/(Surface::getFontHeight()+2);
723 int clistTop = chanListbox.getTopOption();
724 chanListbox.hintSetCurrent(clistTop+row);
726 time_t ttime = cx*60/MINUTE_SCALE+ltime;
727 //x = 155 + (MINUTE_SCALE * (event->time - ltime) / 60);
731 viewman->updateView(this);