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