2 Copyright 2005 Brian Walton
\r
4 This file is part of VOMP.
\r
6 VOMP is free software; you can redistribute it and/or modify
\r
7 it under the terms of the GNU General Public License as published by
\r
8 the Free Software Foundation; either version 2 of the License, or
\r
9 (at your option) any later version.
\r
11 VOMP is distributed in the hope that it will be useful,
\r
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
\r
14 GNU General Public License for more details.
\r
16 You should have received a copy of the GNU General Public License
\r
17 along with VOMP; if not, write to the Free Software
\r
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
\r
21 vepg presents a 2 dimensional electronic programme guide with channels down
\r
22 the y axis and time along the x axis.
\r
23 Programmes are layed on the x axis as alterate coloured blocks with as much
\r
24 of the programme title as will fit inside the block shown as text.
\r
25 Up and down commands step through the channels whilst left and right commands
\r
26 move through the programmes of the currently selected channel.
\r
27 When a programme is selected, it highlights in the grid and full programe details
\r
28 (start time, title and description) are displayed in an area at te top left of the screen.
\r
29 Any currently programmed timers will display in the grid and in the orogramme detail window as red
\r
30 It is possible to select a programme to be recorded by pressing the record button.
\r
31 The video stream currently being viewed is shown as quarter screen in the top right.
\r
36 VEpg* VEpg::instance = NULL;
38 VEpg::VEpg(VVideoLive* v, UINT currentChannel)
\r
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();
\r
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++)
\r
82 // initialise array of pointers to eventlist structures
\r
83 eventLista[listIndex] = NULL;
\r
86 // Create pallet on which to paint our epg view and position it in centre of screen.
\r
87 // Need to reduce size to deal with overscanning TVs.
\r
89 create(xsize, ysize);
\r
90 setScreenPos(xpos, ypos);
\r
93 Colour transparent = Colour(0, 0, 0, 0);
\r
94 setBackgroundColour(transparent);
\r
96 progTitle.setSurface(surface);
\r
97 progTitle.setSurfaceOffset(0,0);
\r
98 progTitle.setDimensions(300,(Surface::getFontHeight() + 4) * 2 + 16); //paragraph line seperation is 4 pixels
\r
99 progTitle.setBackgroundColour(Colour::TITLEBARBACKGROUND);
\r
100 progTitle.setTextPos(5, 16);
103 progInfo.setSurface(surface);
\r
104 progInfo.setSurfaceOffset(0, progTitle.getOffsetY() + progTitle.getHeight());
\r
105 progInfo.setDimensions(300,((Surface::getFontHeight() + 4) * summaryLines) + summaryLowerPadding);
\r
108 chanName.setSurface(surface);
\r
109 chanName.setDimensions(510, (Surface::getFontHeight() + 4));
\r
110 chanName.setSurfaceOffset(305, chanNameYpos);
\r
111 chanName.setBackgroundColour(Colour(0, 0, 0, 90));
\r
113 // create area to display list of channels
\r
114 chanListbox.setSurface(surface); // add channel list
\r
115 chanListbox.setSurfaceOffset(0, progInfo.getOffsetY() + progInfo.getHeight() + Surface::getFontHeight() + 8); // position channel list
\r
116 chanListbox.setDimensions(150, ((Surface::getFontHeight() + 2) * gridRows) + 5); //listbox line seperation is 2 pixels
\r
117 chanListbox.setGap(2);
120 // populate channel list
\r
125 for (UINT i = 0; i < chanList->size(); i++)
\r
127 chan = (*chanList)[i];
\r
128 if (i == currentChannel)
\r
130 chan->index = chanListbox.addOption(chan->name, 0, first);
\r
133 chanName.setText((*chanList)[chanListbox.getCurrentOption()]->name);
136 listTop = chanListbox.getTopOption();
\r
137 chanListbox.draw(); // doing this to allow chanListbox.getBottomOption() in updateEventList() to work
\r
138 time(<ime); // set ltime to now
\r
139 ltime = prevHour(<ime); // set ltime to previous hour TODO make this half hour?
\r
140 time(&selTime); // set selTime to now
\r
141 updateEventList(); // get list of programmes
\r
148 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
\r
150 if (eventLista[listIndex])
\r
152 (eventLista)[listIndex]->clear();
\r
153 delete eventLista[listIndex];
\r
156 // delete [] eventLista;
\r
158 // destroy dynamically allocated memory
\r
161 VEpg* VEpg::getInstance()
166 void VEpg::setInfo(Event* event)
\r
169 struct tm* btime; // to hold programme start and end time
\r
170 char timeString[9]; // to hold programme start and end time
\r
171 int length = strlen(event->title); // calculate length of programme title string
\r
172 char* title = new char[length + 15]; // create string to hold start time, end time and programme title
\r
173 btime = localtime((time_t*)&event->time); //get programme start time
\r
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
\r
180 t = event->time + event->duration; //get programme end time
\r
181 btime = localtime(&t);
\r
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
\r
188 strcat(title, event->title); // then add the programme title
\r
189 progTitle.setText(title); // sput this sring in our text box
\r
190 length = strlen(event->description);
\r
191 char* info = new char[length + 1]; // create programme detail string
\r
192 strcpy(info, event->description);
\r
193 progInfo.setText(info); // show programme detail string
\r
194 // destroy dynamically allocated memory
\r
201 View::draw(); // draw pallet
\r
203 // Moved all the dynamic data drawing to a seperate function
\r
205 // Display the status and key stuff at the bottom
\r
206 int keyx = chanListbox.getOffsetX();
\r
207 int keyy = chanListbox.getOffsetY() + chanListbox.getHeight() + 2;
\r
208 rectangle(keyx, keyy, 605, Surface::getFontHeight() * 2 + 14, Colour(100, 100, 100, 255));
\r
211 w.setSurface(surface);
\r
213 w.nextSymbol = WSymbol::LEFTARROW;
\r
214 w.setSurfaceOffset(keyx + 1, keyy + 20);
\r
217 w.nextSymbol = WSymbol::UP;
\r
218 w.setSurfaceOffset(keyx + 26, keyy + 3);
\r
221 w.nextSymbol = WSymbol::DOWN;
\r
222 w.setSurfaceOffset(keyx + 26, keyy + 36);
\r
225 w.nextSymbol = WSymbol::RIGHTARROW;
\r
226 w.setSurfaceOffset(keyx + 50, keyy + 20);
\r
229 drawText(tr("OK"), keyx + 18, keyy + 20, Colour::LIGHTTEXT);
\r
231 rectangle(keyx + 72, keyy + 4, 104, Surface::getFontHeight() + 2, Colour(200, 0, 0, 255));
\r
232 drawText(tr("Page up"), keyx + 74, keyy + 5, Colour::LIGHTTEXT);
\r
234 rectangle(keyx + 72, keyy + Surface::getFontHeight() + 8, 104, Surface::getFontHeight() + 2, Colour(0, 200, 0, 255));
\r
235 drawText(tr("Page down"), keyx + 74, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
\r
237 rectangle(keyx + 180, keyy + 4, 104, Surface::getFontHeight() + 2, Colour(200, 200, 0, 255));
\r
238 drawText(tr("-24 hours"), keyx + 182, keyy + 5, Colour::LIGHTTEXT);
\r
240 rectangle(keyx + 180, keyy + Surface::getFontHeight() + 8, 104, Surface::getFontHeight() + 2, Colour(0, 0, 200, 255));
\r
241 drawText(tr("+24 hours"), keyx + 182, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
\r
243 rectangle(keyx + 290, keyy + 4, 180, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
\r
244 drawText(tr("Guide / Back: Close"), keyx + 292 , keyy + 5, Colour::LIGHTTEXT);
\r
246 rectangle(keyx + 290, keyy + Surface::getFontHeight() + 8, 180, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
\r
247 Colour red = Colour(130, 0, 0);
\r
248 drawText(tr("Rec: Set timer"), keyx + 292, keyy + Surface::getFontHeight() + 9, red);
\r
250 rectangle(keyx + 474, keyy + 4, 128, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
\r
251 w.nextSymbol = WSymbol::PLAY;
\r
252 w.setSurfaceOffset(keyx + 476, keyy + 5);
\r
254 drawText(tr("Sel channel"), keyx + 496, keyy + 5, Colour::LIGHTTEXT);
\r
256 rectangle(keyx + 474, keyy + Surface::getFontHeight() + 8, 128, Surface::getFontHeight() + 2, Colour(180, 180, 180, 255));
\r
257 drawText(tr("Go: Preview"), keyx + 476, keyy + Surface::getFontHeight() + 9, Colour::LIGHTTEXT);
\r
259 // Draw all the dynamic data
\r
263 void VEpg::drawData()
\r
265 // Not doing View::draw() every time causes
\r
266 // things not to be cleared off the surface properly
\r
267 // So, blank out the data area first
\r
270 chanListbox.getOffsetX(),
\r
271 chanListbox.getOffsetY() - Surface::getFontHeight() - 3,
\r
272 155 + WINDOW_WIDTH * MINUTE_SCALE,
\r
273 chanListbox.getHeight() + Surface::getFontHeight() + 3,
\r
276 chanListbox.draw();
\r
278 chanName.draw(); // TODO this should be dealt with by vvideolive
\r
284 int VEpg::handleCommand(int command)
\r
288 case Remote::DF_UP:
\r
290 { // cursor up the channel list
\r
293 viewman->updateView(this);
\r
296 case Remote::DF_DOWN:
\r
298 { // cursor down the channel list
\r
299 chanListbox.down();
\r
301 viewman->updateView(this);
\r
304 case Remote::DF_LEFT:
\r
306 { // cursor left through time
\r
307 selTime = thisEvent.time - 1;
\r
309 viewman->updateView(this);
\r
312 case Remote::DF_RIGHT:
\r
313 case Remote::RIGHT:
\r
315 // cursor right through time
\r
316 selTime = thisEvent.time + thisEvent.duration;
\r
318 viewman->updateView(this);
\r
323 // cursor up one page
\r
324 chanListbox.pageUp();
\r
326 viewman->updateView(this);
\r
329 case Remote::GREEN:
\r
331 // cursor down one page
\r
332 chanListbox.pageDown();
\r
334 viewman->updateView(this);
\r
339 // step forward 24 hours
\r
340 selTime += 24 * 60 * 60;
\r
342 viewman->updateView(this);
\r
345 case Remote::YELLOW:
\r
347 // step forward 24 hours
\r
348 selTime -= 24 * 60 * 60;
\r
350 viewman->updateView(this);
\r
353 case Remote::RECORD:
\r
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
\r
370 videoLive->channelChange(VVideoLive::NUMBER, (*chanList)[chanListbox.getCurrentOption()]->number);
372 if(command == Remote::GO)
\r
374 // GO just changes channel in preview, PLAY changes channel and returns to normal TV
\r
377 case Remote::GUIDE:
\r
379 // return to normal TV mode
\r
380 if (videoLive) // ptr check done in case being tested from videorec
\r
382 Message* m = new Message(); // Must be done after this view deleted
385 m->message = Message::EPG_CLOSE;
386 ViewMan::getInstance()->postMessage(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
\r
405 void VEpg::drawgrid() // redraws grid and select programme
\r
407 // draw the grid of programmes
\r
408 char timeString[20];
\r
410 time(&t); // set t = now
\r
412 selTime = t; // don't allow cursor in the past
\r
413 if(listTop != chanListbox.getTopOption())
\r
415 // chanListbox has scrolled TODO speed up by changing only rows that have changed
\r
416 listTop = chanListbox.getTopOption();
\r
419 if ((selTime >= ltime + WINDOW_WIDTH * 60) || (selTime <= ltime))
\r
421 // we have cursored back before left time of window
\r
422 //TODO check that this and above don't happen together
\r
423 ltime = prevHour(&selTime);
\r
429 tms = localtime(&t);
\r
430 strftime(timeString, 19, "%a %e %b", tms);
\r
431 int timey = chanListbox.getOffsetY() - Surface::getFontHeight() - 3;
\r
433 drawTextRJ(timeString, timex - 10, timey, Colour::LIGHTTEXT); // print date
\r
434 strftime(timeString, 19, "%H:%M", tms);
\r
435 drawText(timeString, timex, timey, Colour::LIGHTTEXT); // print left time
\r
436 rectangle(155, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
\r
438 tms = localtime(&t);
\r
439 strftime(timeString, 19, "%H:%M", tms);
\r
440 drawText(timeString, timex + 180, timey, Colour::LIGHTTEXT); // print middle time
\r
441 rectangle(335, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
\r
443 tms = localtime(&t);
\r
444 strftime(timeString, 19, "%H:%M", tms);
\r
445 drawText(timeString, timex + 360, timey, Colour::LIGHTTEXT); // print right time
\r
446 rectangle(515, timey + Surface::getFontHeight(), 2, 7, Colour(255, 255, 255, 255));
\r
447 // pointer to selTime
\r
448 rectangle(155 + (selTime - ltime) / 20, timey + Surface::getFontHeight(), 2, 7, Colour(255, 50, 50, 255));
\r
450 // TODO should the above two comditional statements be combined to avoid calling updateEventList() twice?
\r
452 Event noevent; // an event to use if there are gaps in the epg
\r
453 thisEvent.setdescription(tr("There are no programme details available for this period"));
\r
454 thisEvent.duration = WINDOW_WIDTH * 60;
\r
455 thisEvent.time = ltime;
\r
456 thisEvent.settitle(tr("No programme details"));
\r
458 bool swapColour = FALSE; // alternate cell colour
\r
459 bool currentRow = FALSE;
\r
460 int y = chanListbox.getOffsetY() + 5; // vertical position of cell
\r
461 Colour bg, fg; // background colour of cells in grid
\r
462 // for each displayed channel, find programmes that fall in 2.5 hour time window
\r
463 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
\r
465 if (listTop + (int)listIndex >= chanListbox.getBottomOption())
\r
466 continue; // ensure nothing populates grid below last channel
\r
467 currentRow = (listTop + (int)listIndex == chanListbox.getCurrentOption());
\r
468 noevent.time = ltime;
\r
469 noevent.duration = WINDOW_WIDTH * 60;
\r
470 noevent.settitle("");
\r
471 paintCell(&noevent, y, Colour::NOPROGRAMME, Colour::LIGHTTEXT); // fill row with no programme colour to be painted ove with valid programmes
\r
474 thisEvent.setdescription(tr("There are no programme details available for this period"));
\r
475 thisEvent.duration = WINDOW_WIDTH * 60;
\r
476 thisEvent.time = ltime;
\r
477 thisEvent.settitle(tr("No programme details"));
\r
480 if (eventLista[listIndex])
\r
482 sort(eventLista[listIndex]->begin(), eventLista[listIndex]->end(), EventSorter());
\r
483 for(e = 0; e < (eventLista[listIndex])->size(); e++) // step through events for this channel
\r
485 fg = Colour::LIGHTTEXT;
\r
486 event = (*eventLista[listIndex])[e];
\r
489 UINT end = event->time + event->duration; // programme end time
\r
490 if(event->time >= UINT(ltime) + (WINDOW_WIDTH * 60)) // programme starts after RHS of window
\r
491 continue; // that's enough of this channel's events
\r
492 if(end <= UINT(ltime)) // programme ends before LHS of window
\r
493 continue; // this event is before the window - let's try the next event
\r
494 // this event is one we are interested in
\r
495 bg = (swapColour)?Colour::PROGRAMMEA:Colour::PROGRAMMEB; // alternate cell colour
\r
496 swapColour = !swapColour; // it wil be the other colour next time
\r
497 if(event->time <= UINT(selTime) && end > UINT(selTime) && currentRow)
\r
499 // this is the selected programme
\r
500 thisEvent.setdescription(event->description);
\r
501 thisEvent.duration = event->duration;
\r
502 thisEvent.time = event->time;
\r
503 thisEvent.settitle(event->title);
\r
504 thisEvent.id = event->id;
\r
505 if(thisEvent.id == 0)
\r
507 bg = Colour::SELECTHIGHLIGHT; // highlight cell
\r
508 fg = Colour::DARKTEXT;
\r
512 if (currentRow && thisEvent.id == 0)
\r
514 if (end <= UINT(selTime) && end > UINT(thisEvent.time))
\r
515 thisEvent.time = end;
\r
516 if (event->time > UINT(selTime) && event->time < thisEvent.time + thisEvent.duration)
\r
517 thisEvent.duration = event->time - thisEvent.time;
\r
520 paintCell(event, y, bg, fg);
\r
526 // no event list for this channel. Already painted noevent colour so just highlight if selected
\r
529 bg = Colour::SELECTHIGHLIGHT; // highlight cell
\r
530 fg = Colour::DARKTEXT;
\r
531 paintCell(&thisEvent, y, bg, fg);
\r
535 bg = Colour::NOPROGRAMME;
\r
536 fg = Colour::LIGHTTEXT;
\r
537 noevent.settitle(tr("No programme details"));
\r
538 paintCell(&noevent, y, bg, fg);
\r
541 y += Surface::getFontHeight() + 2;
\r
543 setInfo(&thisEvent);
\r
546 void VEpg::updateEventList()
\r
548 if (!chanList) return;
550 for(UINT listIndex = 0; listIndex < gridRows; listIndex++)
\r
552 if(listTop + listIndex >= UINT(chanListbox.getBottomOption()))
\r
554 chan = (*chanList)[listTop + listIndex];
\r
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
\r
560 void VEpg::setCurrentChannel(char* chname)
\r
562 chanName.setText(chname);
\r
564 viewman->updateView(this);
\r
567 void VEpg::paintCell(Event* event, int yOffset, Colour bg, Colour fg)
\r
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
\r
574 UINT end = event->time + event->duration; // programme end time
\r
575 if(event->time <= UINT(ltime) && end > UINT(ltime)) // spans start of displayed window
\r
577 x = 155; // LHS of window
\r
578 if (end > (UINT(ltime) + (WINDOW_WIDTH * 60)))
\r
579 w = WINDOW_WIDTH * MINUTE_SCALE; // spans full 2 hour window
\r
581 w = MINUTE_SCALE * (event->time + event->duration - ltime ) / 60; // get width of remaining programme
\r
583 if((event->time >= UINT(ltime)) && (event->time <= UINT(ltime) + (WINDOW_WIDTH * 60))) // starts within window
\r
585 x = 155 + (MINUTE_SCALE * (event->time - ltime) / 60);
\r
586 w = MINUTE_SCALE * event->duration / 60;
\r
587 //if (w > 155 + MINUTE_SCALE * WINDOW_WIDTH -x)
\r
588 // w = w + x - 155 - MINUTE_SCALE * WINDOW_WIDTH; // ends outside window
\r
590 if (w > 155 + WINDOW_WIDTH * MINUTE_SCALE - x)
\r
591 w = 155 + WINDOW_WIDTH * MINUTE_SCALE -x; // limit cells to RHS of window
\r
592 rectangle(x, y, w, h, bg);
\r
593 char* tt = new char[strlen(event->title) + 1];
\r
594 strcpy (tt, event->title);
\r
596 for (UINT textPos = 0; textPos < strlen(tt); textPos++)
\r
598 int thisCharWidth = surface->getCharWidth(tt[textPos]);
\r
599 if (textWidth + thisCharWidth > w) // text will not fit in cell
\r
601 textWidth = textPos;
\r
604 textWidth += thisCharWidth;
\r
606 char* tT = new char[textWidth];
\r
609 strncpy(tT, tt, textWidth - 1);
\r
610 tT[textWidth - 1] = '\0';
\r
611 surface->drawText(tT, x+2, y, fg.rgba());
\r
617 time_t VEpg::prevHour(time_t* t)
\r
620 tms = localtime(t);
\r
623 return mktime(tms);
\r