fix findBestEvent function
[enigma2.git] / lib / python / Plugins / Extensions / GraphMultiEPG / GraphMultiEpg.py
1 from skin import parseColor
2 from Components.config import config, ConfigClock, ConfigInteger
3 from Components.Pixmap import Pixmap
4 from Components.Button import Button
5 from Components.ActionMap import ActionMap
6 from Components.HTMLComponent import HTMLComponent
7 from Components.GUIComponent import GUIComponent
8 from Components.EpgList import Rect
9 from Components.Sources.Event import Event
10 from Components.Sources.Source import ObsoleteSource
11 from Components.MultiContent import MultiContentEntryText, MultiContentEntryPixmapAlphaTest
12 from Screens.Screen import Screen
13 from Screens.EventView import EventViewSimple
14 from Screens.TimeDateInput import TimeDateInput
15 from Screens.TimerEntry import TimerEntry
16 from Screens.EpgSelection import EPGSelection
17 from Tools.Directories import resolveFilename, SCOPE_SKIN_IMAGE
18 from RecordTimer import RecordTimerEntry, parseEvent
19 from ServiceReference import ServiceReference
20 from enigma import eEPGCache, eListbox, gFont, loadPNG, eListboxPythonMultiContent, \
21         RT_HALIGN_LEFT, RT_HALIGN_CENTER, RT_VALIGN_CENTER, RT_WRAP, eRect, eTimer
22
23 from time import localtime, time, strftime
24
25 class EPGList(HTMLComponent, GUIComponent):
26         def __init__(self, selChangedCB=None, timer = None, time_epoch = 120, overjump_empty=True):
27                 self.cur_event = None
28                 self.cur_service = None
29                 self.offs = 0
30                 self.timer = timer
31                 self.onSelChanged = [ ]
32                 if selChangedCB is not None:
33                         self.onSelChanged.append(selChangedCB)
34                 GUIComponent.__init__(self)
35                 self.l = eListboxPythonMultiContent()
36                 self.l.setItemHeight(54);
37                 self.l.setBuildFunc(self.buildEntry)
38                 if overjump_empty:
39                         self.l.setSelectableFunc(self.isSelectable)
40                 self.epgcache = eEPGCache.getInstance()
41                 self.clock_pixmap = loadPNG(resolveFilename(SCOPE_SKIN_IMAGE, 'epgclock-fs8.png'))
42                 self.time_base = None
43                 self.time_epoch = time_epoch
44                 self.list = None
45                 self.event_rect = None
46
47                 self.foreColor = None
48                 self.foreColorSelected = None
49                 self.borderColor = None
50                 self.backColor = 0x586d88
51                 self.backColorSelected = 0x808080
52
53         def applySkin(self, desktop):
54                 if self.skinAttributes is not None:
55                         attribs = [ ]
56                         for (attrib, value) in self.skinAttributes:
57                                 if attrib == "EntryForegroundColor":
58                                         self.foreColor = parseColor(value).argb()
59                                 elif attrib == "EntryForegroundColorSelected":
60                                         self.foreColorSelected = parseColor(value).argb()
61                                 elif attrib == "EntryBorderColor":
62                                         self.borderColor = parseColor(value).argb()
63                                 elif attrib == "EntryBackgroundColor":
64                                         self.backColor = parseColor(value).argb()
65                                 elif attrib == "EntryBackgroundColorSelected":
66                                         self.backColorSelected = parseColor(value).argb()
67                                 else:
68                                         attribs.append((attrib,value))
69                         self.skinAttributes = attribs
70                 return GUIComponent.applySkin(self, desktop)
71
72         def isSelectable(self, service, sname, event_list):
73                 return (event_list and len(event_list) and True) or False
74
75         def setEpoch(self, epoch):
76                 if self.cur_event is not None and self.cur_service is not None:
77                         self.offs = 0
78                         self.time_epoch = epoch
79                         self.fillMultiEPG(None) # refill
80
81         def getEventFromId(self, service, eventid):
82                 event = None
83                 if self.epgcache is not None and eventid is not None:
84                         event = self.epgcache.lookupEventId(service.ref, eventid)
85                 return event
86
87         def getCurrent(self):
88                 if self.cur_service is None or self.cur_event is None:
89                         return ( None, None )
90                 old_service = self.cur_service  #(service, service_name, events)
91                 events = self.cur_service[2]
92                 refstr = self.cur_service[0]
93                 if not events or not len(events):
94                         return ( None, None )
95                 event = events[self.cur_event] #(event_id, event_title, begin_time, duration)
96                 eventid = event[0]
97                 service = ServiceReference(refstr)
98                 event = self.getEventFromId(service, eventid)
99                 return ( event, service )
100
101         def connectSelectionChanged(func):
102                 if not self.onSelChanged.count(func):
103                         self.onSelChanged.append(func)
104
105         def disconnectSelectionChanged(func):
106                 self.onSelChanged.remove(func)
107
108         def serviceChanged(self):
109                 cur_sel = self.l.getCurrentSelection()
110                 if cur_sel:
111                         self.findBestEvent()
112
113         def findBestEvent(self):
114                 old_service = self.cur_service  #(service, service_name, events)
115                 cur_service = self.cur_service = self.l.getCurrentSelection()
116                 last_time = 0;
117                 time_base = self.getTimeBase()
118                 if old_service and self.cur_event is not None:
119                         events = old_service[2]
120                         cur_event = events[self.cur_event] #(event_id, event_title, begin_time, duration)
121                         last_time = cur_event[2]
122                         if last_time < time_base:
123                                 last_time = time_base
124                 if cur_service:
125                         self.cur_event = 0
126                         events = cur_service[2]
127                         if events and len(events):
128                                 if last_time:
129                                         best_diff = 0
130                                         best = len(events) #set invalid
131                                         idx = 0
132                                         for event in events: #iterate all events
133                                                 ev_time = event[2]
134                                                 if ev_time < time_base:
135                                                         ev_time = time_base
136                                                 diff = abs(ev_time-last_time)
137                                                 if (best == len(events)) or (diff < best_diff):
138                                                         best = idx
139                                                         best_diff = diff
140                                                 idx += 1
141                                         if best != len(events):
142                                                 self.cur_event = best
143                         else:
144                                 self.cur_event = None
145                 self.selEntry(0)
146
147         def selectionChanged(self):
148                 for x in self.onSelChanged:
149                         if x is not None:
150                                 try:
151                                         x()
152                                 except: # FIXME!!!
153                                         print "FIXME in EPGList.selectionChanged"
154                                         pass
155
156         GUI_WIDGET = eListbox
157
158         def postWidgetCreate(self, instance):
159                 instance.setWrapAround(True)
160                 instance.selectionChanged.get().append(self.serviceChanged)
161                 instance.setContent(self.l)
162
163         def recalcEntrySize(self):
164                 esize = self.l.getItemSize()
165                 self.l.setFont(0, gFont("Regular", 20))
166                 self.l.setFont(1, gFont("Regular", 14))
167                 width = esize.width()
168                 height = esize.height()
169                 xpos = 0;
170                 w = width/10*2;
171                 self.service_rect = Rect(xpos, 0, w-10, height)
172                 xpos += w;
173                 w = width/10*8;
174                 self.event_rect = Rect(xpos, 0, w, height)
175                 self.l.setSelectionClip(eRect(0,0,0,0), False)
176
177         def calcEntryPosAndWidthHelper(self, stime, duration, start, end, width):
178                 xpos = (stime - start) * width / (end - start)
179                 ewidth = (stime + duration - start) * width / (end - start)
180                 ewidth -= xpos;
181                 if xpos < 0:
182                         ewidth += xpos;
183                         xpos = 0;
184                 if (xpos+ewidth) > width:
185                         ewidth = width - xpos
186                 return xpos, ewidth
187
188         def calcEntryPosAndWidth(self, event_rect, time_base, time_epoch, ev_start, ev_duration):
189                 xpos, width = self.calcEntryPosAndWidthHelper(ev_start, ev_duration, time_base, time_base + time_epoch * 60, event_rect.width())
190                 return xpos+event_rect.left(), width
191
192         def buildEntry(self, service, service_name, events):
193                 r1=self.service_rect
194                 r2=self.event_rect
195                 res = [ None, MultiContentEntryText(pos = (r1.left(),r1.top()), size = (r1.width(), r1.height()), font = 0, flags = RT_HALIGN_LEFT | RT_VALIGN_CENTER, text = service_name) ]
196
197                 if events:
198                         start = self.time_base+self.offs*self.time_epoch*60
199                         end = start + self.time_epoch * 60
200                         left = r2.left()
201                         top = r2.top()
202                         width = r2.width()
203                         height = r2.height()
204                         foreColor = self.foreColor
205                         foreColorSelected = self.foreColorSelected
206                         backColor = self.backColor
207                         backColorSelected = self.backColorSelected
208                         borderColor = self.borderColor
209
210                         for ev in events:  #(event_id, event_title, begin_time, duration)
211                                 rec=self.timer.isInTimer(ev[0], ev[2], ev[3], service) > ((ev[3]/10)*8)
212                                 xpos, ewidth = self.calcEntryPosAndWidthHelper(ev[2], ev[3], start, end, width)
213                                 res.append(MultiContentEntryText(pos = (left+xpos, top), size = (ewidth, height), font = 1, flags = RT_HALIGN_CENTER | RT_VALIGN_CENTER | RT_WRAP, text = ev[1], color = foreColor, color_sel = foreColorSelected, backcolor = backColor, backcolor_sel = backColorSelected, border_width = 1, border_color = borderColor))
214                                 if rec and ewidth > 23:
215                                         res.append(MultiContentEntryPixmapAlphaTest(pos = (left+xpos+ewidth-22, top+height-22), size = (21, 21), png = self.clock_pixmap, backcolor = backColor, backcolor_selected = backColorSelected))
216                 return res
217
218         def selEntry(self, dir, visible=True):
219                 cur_service = self.cur_service #(service, service_name, events)
220                 if not self.event_rect:
221                         self.recalcEntrySize()
222                 if cur_service and self.cur_event is not None:
223                         update = True
224                         entries = cur_service[2]
225                         if dir == 0: #current
226                                 update = False
227                         elif dir == +1: #next
228                                 if self.cur_event+1 < len(entries):
229                                         self.cur_event+=1
230                                 else:
231                                         self.offs += 1
232                                         self.fillMultiEPG(None) # refill
233                                         return True
234                         elif dir == -1: #prev
235                                 if self.cur_event-1 >= 0:
236                                         self.cur_event-=1
237                                 elif self.offs > 0:
238                                         self.offs -= 1
239                                         self.fillMultiEPG(None) # refill
240                                         return True
241                         entry = entries[self.cur_event] #(event_id, event_title, begin_time, duration)
242                         time_base = self.time_base+self.offs*self.time_epoch*60
243                         xpos, width = self.calcEntryPosAndWidth(self.event_rect, time_base, self.time_epoch, entry[2], entry[3])
244                         self.l.setSelectionClip(eRect(xpos, 0, width, self.event_rect.height()), visible and update)
245                 else:
246                         self.l.setSelectionClip(eRect(self.event_rect.left(), self.event_rect.top(), self.event_rect.width(), self.event_rect.height()), False)
247                 self.selectionChanged()
248                 return False
249
250         def queryEPG(self, list, buildFunc=None):
251                 if self.epgcache is not None:
252                         if buildFunc is not None:
253                                 return self.epgcache.lookupEvent(list, buildFunc)
254                         else:
255                                 return self.epgcache.lookupEvent(list)
256                 return [ ]
257
258         def fillMultiEPG(self, services, stime=-1):
259                 if services is None:
260                         time_base = self.time_base+self.offs*self.time_epoch*60
261                         test = [ (service[0], 0, time_base, self.time_epoch) for service in self.list ]
262                 else:
263                         self.cur_event = None
264                         self.cur_service = None
265                         self.time_base = int(stime)
266                         test = [ (service.ref.toString(), 0, self.time_base, self.time_epoch) for service in services ]
267                 test.insert(0, 'RnITBD')
268                 epg_data = self.queryEPG(test)
269
270                 self.list = [ ]
271                 tmp_list = None
272                 service = ""
273                 sname = ""
274                 for x in epg_data:
275                         if service != x[0]:
276                                 if tmp_list is not None:
277                                         self.list.append((service, sname, tmp_list[0][0] and tmp_list))
278                                 service = x[0]
279                                 sname = x[1]
280                                 tmp_list = [ ]
281                         tmp_list.append((x[2], x[3], x[4], x[5]))
282                 if tmp_list and len(tmp_list):
283                         self.list.append((service, sname, tmp_list[0][0] and tmp_list))
284
285                 self.l.setList(self.list)
286                 self.findBestEvent()
287
288         def getEventRect(self):
289                 rc = self.event_rect
290                 return Rect( rc.left() + (self.instance and self.instance.position().x() or 0), rc.top(), rc.width(), rc.height() )
291
292         def getTimeEpoch(self):
293                 return self.time_epoch
294
295         def getTimeBase(self):
296                 return self.time_base + (self.offs * self.time_epoch * 60)
297
298         def resetOffset(self):
299                 self.offs = 0
300
301 class TimelineText(HTMLComponent, GUIComponent):
302         def __init__(self):
303                 GUIComponent.__init__(self)
304                 self.l = eListboxPythonMultiContent()
305                 self.l.setSelectionClip(eRect(0,0,0,0))
306                 self.l.setItemHeight(25);
307                 self.l.setFont(0, gFont("Regular", 20))
308
309         GUI_WIDGET = eListbox
310
311         def postWidgetCreate(self, instance):
312                 instance.setContent(self.l)
313
314         def setEntries(self, entries):
315                 res = [ None ] # no private data needed
316                 for x in entries:
317                         tm = x[0]
318                         xpos = x[1]
319                         str = strftime("%H:%M", localtime(tm))
320                         res.append((eListboxPythonMultiContent.TYPE_TEXT, xpos-30, 0, 60, 25, 0, RT_HALIGN_CENTER|RT_VALIGN_CENTER, str))
321                 self.l.setList([res])
322
323 config.misc.graph_mepg_prev_time=ConfigClock(default = time())
324 config.misc.graph_mepg_prev_time_period=ConfigInteger(default=120, limits=(60,300))
325
326 class GraphMultiEPG(Screen):
327         def __init__(self, session, services, zapFunc=None, bouquetChangeCB=None):
328                 Screen.__init__(self, session)
329                 self.bouquetChangeCB = bouquetChangeCB
330                 now = time()
331                 tmp = now % 900
332                 self.ask_time = now - tmp
333                 self.closeRecursive = False
334                 self["key_red"] = Button("")
335                 self["key_green"] = Button(_("Add timer"))
336                 self["timeline_text"] = TimelineText()
337                 self["Event"] = Event()
338                 self["Clock"] = ObsoleteSource(new_source = "global.CurrentTime", removal_date = "2008-01")
339                 self.time_lines = [ ]
340                 for x in (0,1,2,3,4,5):
341                         pm = Pixmap()
342                         self.time_lines.append(pm)
343                         self["timeline%d"%(x)] = pm
344                 self["timeline_now"] = Pixmap()
345                 self.services = services
346                 self.zapFunc = zapFunc
347
348                 self["list"] = EPGList(selChangedCB = self.onSelectionChanged, timer = self.session.nav.RecordTimer, time_epoch = config.misc.graph_mepg_prev_time_period.value )
349
350                 self["actions"] = ActionMap(["EPGSelectActions", "OkCancelActions"],
351                         {
352                                 "cancel": self.closeScreen,
353                                 "ok": self.eventSelected,
354                                 "timerAdd": self.timerAdd,
355                                 "info": self.infoKeyPressed,
356                                 "red": self.zapTo,
357                                 "input_date_time": self.enterDateTime,
358                                 "nextBouquet": self.nextBouquet,
359                                 "prevBouquet": self.prevBouquet,
360                         })
361                 self["actions"].csel = self
362
363                 self["input_actions"] = ActionMap(["InputActions"],
364                         {
365                                 "left": self.leftPressed,
366                                 "right": self.rightPressed,
367                                 "1": self.key1,
368                                 "2": self.key2,
369                                 "3": self.key3,
370                                 "4": self.key4,
371                                 "5": self.key5,
372                         },-1)
373
374                 self.updateTimelineTimer = eTimer()
375                 self.updateTimelineTimer.timeout.get().append(self.moveTimeLines)
376                 self.updateTimelineTimer.start(60*1000)
377                 self.onLayoutFinish.append(self.onCreate)
378
379         def leftPressed(self):
380                 self.prevEvent()
381
382         def rightPressed(self):
383                 self.nextEvent()
384
385         def nextEvent(self, visible=True):
386                 ret = self["list"].selEntry(+1, visible)
387                 if ret:
388                         self.moveTimeLines(True)
389
390         def prevEvent(self, visible=True):
391                 ret = self["list"].selEntry(-1, visible)
392                 if ret:
393                         self.moveTimeLines(True)
394
395         def key1(self):
396                 self["list"].setEpoch(60)
397                 config.misc.graph_mepg_prev_time_period.value = 60
398                 self.moveTimeLines()
399
400         def key2(self):
401                 self["list"].setEpoch(120)
402                 config.misc.graph_mepg_prev_time_period.value = 120
403                 self.moveTimeLines()
404
405         def key3(self):
406                 self["list"].setEpoch(180)
407                 config.misc.graph_mepg_prev_time_period.value = 180
408                 self.moveTimeLines()
409
410         def key4(self):
411                 self["list"].setEpoch(240)
412                 config.misc.graph_mepg_prev_time_period.value = 240
413                 self.moveTimeLines()
414
415         def key5(self):
416                 self["list"].setEpoch(300)
417                 config.misc.graph_mepg_prev_time_period.value = 300
418                 self.moveTimeLines()
419
420         def nextBouquet(self):
421                 if self.bouquetChangeCB:
422                         self.bouquetChangeCB(1, self)
423
424         def prevBouquet(self):
425                 if self.bouquetChangeCB:
426                         self.bouquetChangeCB(-1, self)
427
428         def enterDateTime(self):
429                 self.session.openWithCallback(self.onDateTimeInputClosed, TimeDateInput, config.misc.graph_mepg_prev_time )
430
431         def onDateTimeInputClosed(self, ret):
432                 if len(ret) > 1:
433                         if ret[0]:
434                                 self.ask_time=ret[1]
435                                 l = self["list"]
436                                 l.resetOffset()
437                                 l.fillMultiEPG(self.services, ret[1])
438                                 self.moveTimeLines(True)
439
440         def closeScreen(self):
441                 self.close(self.closeRecursive)
442
443         def infoKeyPressed(self):
444                 cur = self["list"].getCurrent()
445                 event = cur[0]
446                 service = cur[1]
447                 if event is not None:
448                         self.session.open(EventViewSimple, event, service, self.eventViewCallback, self.openSimilarList)
449
450         def openSimilarList(self, eventid, refstr):
451                 self.session.open(EPGSelection, refstr, None, eventid)
452
453         def setServices(self, services):
454                 self.services = services
455                 self.onCreate()
456
457         #just used in multipeg
458         def onCreate(self):
459                 self["list"].fillMultiEPG(self.services, self.ask_time)
460                 self.moveTimeLines()
461
462         def eventViewCallback(self, setEvent, setService, val):
463                 l = self["list"]
464                 old = l.getCurrent()
465                 if val == -1:
466                         self.prevEvent(False)
467                 elif val == +1:
468                         self.nextEvent(False)
469                 cur = l.getCurrent()
470                 if cur[0] is None and cur[1].ref != old[1].ref:
471                         self.eventViewCallback(setEvent, setService, val)
472                 else:
473                         setService(cur[1])
474                         setEvent(cur[0])
475
476         def zapTo(self):
477                 if self.zapFunc and self["key_red"].getText() == "Zap":
478                         self.closeRecursive = True
479                         ref = self["list"].getCurrent()[1]
480                         self.zapFunc(ref.ref)
481
482         def eventSelected(self):
483                 self.infoKeyPressed()
484
485         def timerAdd(self):
486                 cur = self["list"].getCurrent()
487                 event = cur[0]
488                 serviceref = cur[1]
489                 if event is None:
490                         return
491                 newEntry = RecordTimerEntry(serviceref, checkOldTimers = True, *parseEvent(event))
492                 self.session.openWithCallback(self.timerEditFinished, TimerEntry, newEntry)
493
494         def timerEditFinished(self, answer):
495                 if answer[0]:
496                         self.session.nav.RecordTimer.record(answer[1])
497                 else:
498                         print "Timeredit aborted"
499
500         def onSelectionChanged(self):
501                 evt = self["list"].getCurrent()
502                 self["Event"].newEvent(evt and evt[0])
503                 if evt and evt[0]:
504                         evt = evt[0]
505                         now = time()
506                         start = evt.getBeginTime()
507                         end = start + evt.getDuration()
508                         if now >= start and now <= end:
509                                 self["key_red"].setText("Zap")
510                         else:
511                                 self["key_red"].setText("")
512
513         def moveTimeLines(self, force=False):
514                 l = self["list"]
515                 event_rect = l.getEventRect()
516                 time_epoch = l.getTimeEpoch()
517                 time_base = l.getTimeBase()
518                 if event_rect is None or time_epoch is None or time_base is None:
519                         return
520                 time_steps = time_epoch > 180 and 60 or 30
521                 num_lines = time_epoch/time_steps
522                 incWidth=event_rect.width()/num_lines
523                 pos=event_rect.left()
524                 timeline_entries = [ ]
525                 x = 0
526                 changecount = 0
527                 for line in self.time_lines:
528                         old_pos = line.position
529                         new_pos = (x == num_lines and event_rect.left()+event_rect.width() or pos, old_pos[1])
530                         if not x or x >= num_lines:
531                                 line.visible = False
532                         else:
533                                 if old_pos != new_pos:
534                                         line.setPosition(new_pos[0], new_pos[1])
535                                         changecount += 1
536                                 line.visible = True
537                         if not x or line.visible:
538                                 timeline_entries.append((time_base + x * time_steps * 60, new_pos[0]))
539                         x += 1
540                         pos += incWidth
541
542                 if changecount or force:
543                         self["timeline_text"].setEntries(timeline_entries)
544
545                 now=time()
546                 timeline_now = self["timeline_now"]
547                 if now >= time_base and now < (time_base + time_epoch * 60):
548                         bla = (event_rect.width() * 1000) / time_epoch
549                         xpos = ((now/60) - (time_base/60)) * bla / 1000
550                         old_pos = timeline_now.position
551                         new_pos = (xpos+event_rect.left(), old_pos[1])
552                         if old_pos != new_pos:
553                                 timeline_now.setPosition(new_pos[0], new_pos[1])
554                         timeline_now.visible = True
555                 else:
556                         timeline_now.visible = False