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)
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(VDR::VIDEO); //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
148 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
150 if (eventLista[listIndex])
152 (eventLista)[listIndex]->clear();
153 delete eventLista[listIndex];
156 // delete [] eventLista;
158 // destroy dynamically allocated memory
161 VEpg* VEpg::getInstance()
166 void VEpg::setInfo(Event* event)
169 struct tm* btime; // to hold programme start and end time
170 char timeString[9]; // to hold programme start and end time
171 int length = strlen(event->title); // calculate length of programme title string
172 char* title = new char[length + 15]; // create string to hold start time, end time and programme title
173 btime = localtime((time_t*)&event->time); //get programme start time
175 strftime(timeString, 9, "%0H:%0M - ", btime); // and format it as hh:mm -
177 strftime(timeString, 9, "%H:%M - ", btime); // and format it as hh:mm -
179 strcpy(title, timeString); // put it in our buffer
180 t = event->time + event->duration; //get programme end time
181 btime = localtime(&t);
183 strftime(timeString, 7, "%0H:%0M ", btime); // and format it as hh:mm -
185 strftime(timeString, 7, "%H:%M ", btime); // and format it as hh:mm -
187 strcat(title, timeString); // put it in our buffer
188 strcat(title, event->title); // then add the programme title
189 progTitle.setText(title); // sput this sring in our text box
190 length = strlen(event->description);
191 char* info = new char[length + 1]; // create programme detail string
192 strcpy(info, event->description);
193 progInfo.setText(info); // show programme detail string
194 // destroy dynamically allocated memory
201 View::draw(); // draw pallet
203 // Moved all the dynamic data drawing to a seperate function
205 // Display the status and key stuff at the bottom
206 int keyx = chanListbox.getOffsetX();
207 int keyy = chanListbox.getOffsetY() + chanListbox.getHeight() + 2;
208 rectangle(keyx, keyy, 605, Surface::getFontHeight() * 2 + 14, Colour(100, 100, 100, 255));
211 w.setSurface(surface);
213 w.nextSymbol = WSymbol::LEFTARROW;
214 w.setSurfaceOffset(keyx + 1, keyy + 20);
217 w.nextSymbol = WSymbol::UP;
218 w.setSurfaceOffset(keyx + 26, keyy + 3);
221 w.nextSymbol = WSymbol::DOWN;
222 w.setSurfaceOffset(keyx + 26, keyy + 36);
225 w.nextSymbol = WSymbol::RIGHTARROW;
226 w.setSurfaceOffset(keyx + 50, keyy + 20);
229 drawText(tr("OK"), keyx + 18, keyy + 20, Colour::LIGHTTEXT);
231 rectangle(keyx + 72, keyy + 4, 104, Surface::getFontHeight() + 2, Colour(200, 0, 0, 255));
232 drawText(tr("Page up"), keyx + 74, keyy + 5, Colour::LIGHTTEXT);
234 rectangle(keyx + 72, keyy + Surface::getFontHeight() + 8, 104, Surface::getFontHeight() + 2, Colour(0, 200, 0, 255));
235 drawText(tr("Page down"), keyx + 74, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
237 rectangle(keyx + 180, keyy + 4, 104, Surface::getFontHeight() + 2, Colour(200, 200, 0, 255));
238 drawText(tr("-24 hours"), keyx + 182, keyy + 5, Colour::LIGHTTEXT);
240 rectangle(keyx + 180, keyy + Surface::getFontHeight() + 8, 104, Surface::getFontHeight() + 2, Colour(0, 0, 200, 255));
241 drawText(tr("+24 hours"), keyx + 182, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
243 rectangle(keyx + 290, keyy + 4, 180, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
244 drawText(tr("Guide / Back: Close"), keyx + 292 , keyy + 5, Colour::LIGHTTEXT);
246 rectangle(keyx + 290, keyy + Surface::getFontHeight() + 8, 180, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
247 Colour red = Colour(130, 0, 0);
248 drawText(tr("Rec: Set timer"), keyx + 292, keyy + Surface::getFontHeight() + 9, red);
250 rectangle(keyx + 474, keyy + 4, 128, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
251 w.nextSymbol = WSymbol::PLAY;
252 w.setSurfaceOffset(keyx + 476, keyy + 5);
254 drawText(tr("Sel channel"), keyx + 496, keyy + 5, Colour::LIGHTTEXT);
256 rectangle(keyx + 474, keyy + Surface::getFontHeight() + 8, 128, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
257 drawText(tr("Go: Preview"), keyx + 476, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
259 // Draw all the dynamic data
263 void VEpg::drawData()
265 // Not doing View::draw() every time causes
266 // things not to be cleared off the surface properly
267 // So, blank out the data area first
270 chanListbox.getOffsetX(),
271 chanListbox.getOffsetY() - Surface::getFontHeight() - 3,
272 155 + WINDOW_WIDTH * MINUTE_SCALE,
273 chanListbox.getHeight() + Surface::getFontHeight() + 3,
278 chanName.draw(); // TODO this should be dealt with by vvideolive
284 int VEpg::handleCommand(int command)
290 { // cursor up the channel list
293 viewman->updateView(this);
296 case Remote::DF_DOWN:
298 { // cursor down the channel list
301 viewman->updateView(this);
304 case Remote::DF_LEFT:
306 { // cursor left through time
307 selTime = thisEvent.time - 1;
309 viewman->updateView(this);
312 case Remote::DF_RIGHT:
315 // cursor right through time
316 selTime = thisEvent.time + thisEvent.duration;
318 viewman->updateView(this);
323 // cursor up one page
324 chanListbox.pageUp();
326 viewman->updateView(this);
331 // cursor down one page
332 chanListbox.pageDown();
334 viewman->updateView(this);
339 // step forward 24 hours
340 selTime += 24 * 60 * 60;
342 viewman->updateView(this);
347 // step forward 24 hours
348 selTime -= 24 * 60 * 60;
350 viewman->updateView(this);
355 if (!chanList) return 2;
356 Log::getInstance()->log("VEPG", Log::DEBUG, "ID %lu TIME %lu DURATION %lu TITLE %s", thisEvent.id, thisEvent.time, thisEvent.duration, thisEvent.title);
357 VEpgSetTimer* vs = new VEpgSetTimer(&thisEvent, (*chanList)[chanListbox.getCurrentOption()]);
360 viewman->updateView(vs);
367 if (!chanList) return 2;
369 // select programme and display menu TODO currently just changes to selected channel
370 videoLive->channelChange(VVideoLive::NUMBER, (*chanList)[chanListbox.getCurrentOption()]->number);
372 if(command == Remote::GO)
374 // GO just changes channel in preview, PLAY changes channel and returns to normal TV
379 // return to normal TV mode
380 if (videoLive) // ptr check done in case being tested from videorec
382 Message* m = new Message(); // Must be done after this view deleted
385 m->message = Message::EPG_CLOSE;
386 Command::getInstance()->postMessageNoLock(m);
390 case Remote::CHANNELUP:
392 videoLive->channelChange(VVideoLive::OFFSET, VVideoLive::UP);
395 case Remote::CHANNELDOWN:
397 videoLive->channelChange(VVideoLive::OFFSET, VVideoLive::DOWN);
401 // stop command getting to any more views
405 void VEpg::drawgrid() // redraws grid and select programme
407 // draw the grid of programmes
410 time(&t); // set t = now
412 selTime = t; // don't allow cursor in the past
413 if(listTop != chanListbox.getTopOption())
415 // chanListbox has scrolled TODO speed up by changing only rows that have changed
416 listTop = chanListbox.getTopOption();
419 if ((selTime >= ltime + WINDOW_WIDTH * 60) || (selTime <= ltime))
421 // we have cursored back before left time of window
422 //TODO check that this and above don't happen together
423 ltime = prevHour(&selTime);
430 strftime(timeString, 19, "%a %e %b", tms);
431 int timey = chanListbox.getOffsetY() - Surface::getFontHeight() - 3;
433 drawTextRJ(timeString, timex - 10, timey, Colour::LIGHTTEXT); // print date
434 strftime(timeString, 19, "%H:%M", tms);
435 drawText(timeString, timex, timey, Colour::LIGHTTEXT); // print left time
436 rectangle(155, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
439 strftime(timeString, 19, "%H:%M", tms);
440 drawText(timeString, timex + 180, timey, Colour::LIGHTTEXT); // print middle time
441 rectangle(335, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
444 strftime(timeString, 19, "%H:%M", tms);
445 drawText(timeString, timex + 360, timey, Colour::LIGHTTEXT); // print right time
446 rectangle(515, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
447 // pointer to selTime
448 rectangle(155 + (selTime - ltime) / 20, timey + Surface::getFontHeight(), 2, 7, Colour(255, 50, 50, 255));
450 // TODO should the above two comditional statements be combined to avoid calling updateEventList() twice?
452 Event noevent; // an event to use if there are gaps in the epg
453 thisEvent.setdescription(tr("There are no programme details available for this period"));
454 thisEvent.duration = WINDOW_WIDTH * 60;
455 thisEvent.time = ltime;
456 thisEvent.settitle(tr("No programme details"));
458 bool swapColour = FALSE; // alternate cell colour
459 bool currentRow = FALSE;
460 int y = chanListbox.getOffsetY() + 5; // vertical position of cell
461 Colour bg, fg; // background colour of cells in grid
462 // for each displayed channel, find programmes that fall in 2.5 hour time window
463 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
465 if (listTop + (int)listIndex >= chanListbox.getBottomOption())
466 continue; // ensure nothing populates grid below last channel
467 currentRow = (listTop + (int)listIndex == chanListbox.getCurrentOption());
468 noevent.time = ltime;
469 noevent.duration = WINDOW_WIDTH * 60;
470 noevent.settitle("");
471 paintCell(&noevent, y, Colour::NOPROGRAMME, Colour::LIGHTTEXT); // fill row with no programme colour to be painted ove with valid programmes
474 thisEvent.setdescription(tr("There are no programme details available for this period"));
475 thisEvent.duration = WINDOW_WIDTH * 60;
476 thisEvent.time = ltime;
477 thisEvent.settitle(tr("No programme details"));
480 if (eventLista[listIndex])
482 sort(eventLista[listIndex]->begin(), eventLista[listIndex]->end(), EventSorter());
483 for(e = 0; e < (eventLista[listIndex])->size(); e++) // step through events for this channel
485 fg = Colour::LIGHTTEXT;
486 event = (*eventLista[listIndex])[e];
489 UINT end = event->time + event->duration; // programme end time
490 if(event->time >= UINT(ltime) + (WINDOW_WIDTH * 60)) // programme starts after RHS of window
491 continue; // that's enough of this channel's events
492 if(end <= UINT(ltime)) // programme ends before LHS of window
493 continue; // this event is before the window - let's try the next event
494 // this event is one we are interested in
495 bg = (swapColour)?Colour::PROGRAMMEA:Colour::PROGRAMMEB; // alternate cell colour
496 swapColour = !swapColour; // it wil be the other colour next time
497 if(event->time <= UINT(selTime) && end > UINT(selTime) && currentRow)
499 // this is the selected programme
500 thisEvent.setdescription(event->description);
501 thisEvent.duration = event->duration;
502 thisEvent.time = event->time;
503 thisEvent.settitle(event->title);
504 thisEvent.id = event->id;
505 if(thisEvent.id == 0)
507 bg = Colour::SELECTHIGHLIGHT; // highlight cell
508 fg = Colour::DARKTEXT;
512 if (currentRow && thisEvent.id == 0)
514 if (end <= UINT(selTime) && end > UINT(thisEvent.time))
515 thisEvent.time = end;
516 if (event->time > UINT(selTime) && event->time < thisEvent.time + thisEvent.duration)
517 thisEvent.duration = event->time - thisEvent.time;
520 paintCell(event, y, bg, fg);
526 // no event list for this channel. Already painted noevent colour so just highlight if selected
529 bg = Colour::SELECTHIGHLIGHT; // highlight cell
530 fg = Colour::DARKTEXT;
531 paintCell(&thisEvent, y, bg, fg);
535 bg = Colour::NOPROGRAMME;
536 fg = Colour::LIGHTTEXT;
537 noevent.settitle(tr("No programme details"));
538 paintCell(&noevent, y, bg, fg);
541 y += Surface::getFontHeight() + 2;
546 void VEpg::updateEventList()
548 if (!chanList) return;
550 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
552 if(listTop + listIndex >= UINT(chanListbox.getBottomOption()))
554 chan = (*chanList)[listTop + listIndex];
556 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
560 void VEpg::setCurrentChannel(char* chname)
562 chanName.setText(chname);
564 viewman->updateView(this);
567 void VEpg::paintCell(Event* event, int yOffset, Colour bg, Colour fg)
570 w = x = 0; // keep compiler happy
573 h = Surface::getFontHeight(); // TODO if want border around text, need to increae this and wselectlist line height
574 UINT end = event->time + event->duration; // programme end time
575 if(event->time <= UINT(ltime) && end > UINT(ltime)) // spans start of displayed window
577 x = 155; // LHS of window
578 if (end > (UINT(ltime) + (WINDOW_WIDTH * 60)))
579 w = WINDOW_WIDTH * MINUTE_SCALE; // spans full 2 hour window
581 w = MINUTE_SCALE * (event->time + event->duration - ltime ) / 60; // get width of remaining programme
583 if((event->time >= UINT(ltime)) && (event->time <= UINT(ltime) + (WINDOW_WIDTH * 60))) // starts within window
585 x = 155 + (MINUTE_SCALE * (event->time - ltime) / 60);
586 w = MINUTE_SCALE * event->duration / 60;
587 //if (w > 155 + MINUTE_SCALE * WINDOW_WIDTH -x)
588 // w = w + x - 155 - MINUTE_SCALE * WINDOW_WIDTH; // ends outside window
590 if (w > 155 + WINDOW_WIDTH * MINUTE_SCALE - x)
591 w = 155 + WINDOW_WIDTH * MINUTE_SCALE -x; // limit cells to RHS of window
592 rectangle(x, y, w, h, bg);
593 char* tt = new char[strlen(event->title) + 1];
594 strcpy (tt, event->title);
596 for (UINT textPos = 0; textPos < strlen(tt); textPos++)
598 int thisCharWidth = surface->getCharWidth(tt[textPos]);
599 if (textWidth + thisCharWidth > w) // text will not fit in cell
604 textWidth += thisCharWidth;
606 char* tT = new char[textWidth];
609 strncpy(tT, tt, textWidth - 1);
610 tT[textWidth - 1] = '\0';
611 surface->drawText(tT, x+2, y, fg.rgba());
617 time_t VEpg::prevHour(time_t* t)
626 void VEpg::processMessage(Message* m)
628 if (m->message == Message::MOUSE_MOVE)
630 if (chanListbox.mouseMove((m->parameter>>16)-getScreenX(),(m->parameter&0xFFFF)-getScreenY()))
633 ViewMan::getInstance()->updateView(this);
636 else if (m->message == Message::MOUSE_LBDOWN)
638 if (chanListbox.mouseLBDOWN((m->parameter>>16)-getScreenX(),(m->parameter&0xFFFF)-getScreenY()))
640 ViewMan::getInstance()->handleCommand(Remote::OK); //simulate OK press
644 //check if press is outside this view! then simulate cancel
645 int x=(m->parameter>>16)-getScreenX();
646 int y=(m->parameter&0xFFFF)-getScreenY();
647 int keyx = chanListbox.getOffsetX();
648 int keyy = chanListbox.getOffsetY() + chanListbox.getHeight() + 2;
650 if (x<0 || y <0 || x>getWidth() || y>getHeight())
652 ViewMan::getInstance()->handleCommand(Remote::BACK); //simulate cancel press
654 else if (x>=(keyx+72) && y>=(keyy+4) &&x<=(keyx+72+104) &&y<=(keyy+4+Surface::getFontHeight() + 2))
656 ViewMan::getInstance()->handleCommand(Remote::RED);
658 else if (x>=(keyx+72) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+72+104) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
660 ViewMan::getInstance()->handleCommand(Remote::GREEN);
662 else if (x>=(keyx+180) && y>=(keyy+4) &&x<=(keyx+180+104) &&y<=(keyy+4+Surface::getFontHeight() + 2))
664 ViewMan::getInstance()->handleCommand(Remote::YELLOW);
666 else if (x>=(keyx+180) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+180+104) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
668 ViewMan::getInstance()->handleCommand(Remote::BLUE);
670 else if (x>=(keyx+290) && y>=(keyy+4) &&x<=(keyx+180+290) &&y<=(keyy+4+Surface::getFontHeight() + 2))
672 ViewMan::getInstance()->handleCommand(Remote::BACK);
674 else if (x>=(keyx+290) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+290+180) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
676 ViewMan::getInstance()->handleCommand(Remote::RECORD);
678 else if (x>=(keyx+474) && y>=(keyy+4) &&x<=(keyx+128+474) &&y<=(keyy+4+Surface::getFontHeight() + 2))
680 ViewMan::getInstance()->handleCommand(Remote::PLAY);
682 else if (x>=(keyx+474) && y>=(keyy+ Surface::getFontHeight() + 8) &&x<=(keyx+238+474) &&y<=(keyy+8+2*Surface::getFontHeight() + 2))
684 ViewMan::getInstance()->handleCommand(Remote::GO);
686 else if ( x>=(chanListbox.getOffsetX())
687 && y>=(chanListbox.getOffsetY() + 5)
688 // &&x<=(chanListbox.getOffsetX()+155 + WINDOW_WIDTH * MINUTE_SCALE)
689 &&y<=(chanListbox.getOffsetY() - Surface::getFontHeight()
690 - 3+chanListbox.getHeight() + Surface::getFontHeight() + 3)
693 int cy=y-(chanListbox.getOffsetY() + 5);
694 int row=cy/(Surface::getFontHeight()+2);
695 int clistTop = chanListbox.getTopOption();
696 chanListbox.hintSetCurrent(clistTop+row);
698 time_t ttime = cx*60/MINUTE_SCALE+ltime;
699 //x = 155 + (MINUTE_SCALE * (event->time - ltime) / 60);
703 viewman->updateView(this);