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