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