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