GraphMultiEpg: fix zap to services without events
[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(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) ]
208
209                 if events:
210                         start = self.time_base+self.offs*self.time_epoch*60
211                         end = start + self.time_epoch * 60
212                         left = r2.left()
213                         top = r2.top()
214                         width = r2.width()
215                         height = r2.height()
216                         foreColor = self.foreColor
217                         foreColorSelected = self.foreColorSelected
218                         backColor = self.backColor
219                         backColorSelected = self.backColorSelected
220                         borderColor = self.borderColor
221
222                         for ev in events:  #(event_id, event_title, begin_time, duration)
223                                 rec=ev[2] and self.timer.isInTimer(ev[0], ev[2], ev[3], service) > ((ev[3]/10)*8)
224                                 xpos, ewidth = self.calcEntryPosAndWidthHelper(ev[2], ev[3], start, end, width)
225                                 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))
226                                 if rec and ewidth > 23:
227                                         res.append(MultiContentEntryPixmapAlphaTest(pos = (left+xpos+ewidth-22, top+height-22), size = (21, 21), png = self.clock_pixmap, backcolor = backColor, backcolor_sel = backColorSelected))
228                 return res
229
230         def selEntry(self, dir, visible=True):
231                 cur_service = self.cur_service #(service, service_name, events)
232                 self.recalcEntrySize()
233                 valid_event = self.cur_event is not None
234                 if cur_service:
235                         update = True
236                         entries = cur_service[2]
237                         if dir == 0: #current
238                                 update = False
239                         elif dir == +1: #next
240                                 if valid_event and self.cur_event+1 < len(entries):
241                                         self.cur_event+=1
242                                 else:
243                                         self.offs += 1
244                                         self.fillMultiEPG(None) # refill
245                                         return True
246                         elif dir == -1: #prev
247                                 if valid_event and self.cur_event-1 >= 0:
248                                         self.cur_event-=1
249                                 elif self.offs > 0:
250                                         self.offs -= 1
251                                         self.fillMultiEPG(None) # refill
252                                         return True
253                 if cur_service and valid_event:
254                         entry = entries[self.cur_event] #(event_id, event_title, begin_time, duration)
255                         time_base = self.time_base+self.offs*self.time_epoch*60
256                         xpos, width = self.calcEntryPosAndWidth(self.event_rect, time_base, self.time_epoch, entry[2], entry[3])
257                         self.l.setSelectionClip(eRect(xpos, 0, width, self.event_rect.height()), visible and update)
258                 else:
259                         self.l.setSelectionClip(eRect(self.event_rect.left(), self.event_rect.top(), self.event_rect.width(), self.event_rect.height()), False)
260                 self.selectionChanged()
261                 return False
262
263         def queryEPG(self, list, buildFunc=None):
264                 if self.epgcache is not None:
265                         if buildFunc is not None:
266                                 return self.epgcache.lookupEvent(list, buildFunc)
267                         else:
268                                 return self.epgcache.lookupEvent(list)
269                 return [ ]
270
271         def fillMultiEPG(self, services, stime=-1):
272                 if services is None:
273                         time_base = self.time_base+self.offs*self.time_epoch*60
274                         test = [ (service[0], 0, time_base, self.time_epoch) for service in self.list ]
275                 else:
276                         self.cur_event = None
277                         self.cur_service = None
278                         self.time_base = int(stime)
279                         test = [ (service.ref.toString(), 0, self.time_base, self.time_epoch) for service in services ]
280                 test.insert(0, 'XRnITBD')
281                 epg_data = self.queryEPG(test)
282
283                 self.list = [ ]
284                 tmp_list = None
285                 service = ""
286                 sname = ""
287                 for x in epg_data:
288                         if service != x[0]:
289                                 if tmp_list is not None:
290                                         self.list.append((service, sname, tmp_list[0][0] is not None and tmp_list or None))
291                                 service = x[0]
292                                 sname = x[1]
293                                 tmp_list = [ ]
294                         tmp_list.append((x[2], x[3], x[4], x[5]))
295                 if tmp_list and len(tmp_list):
296                         self.list.append((service, sname, tmp_list[0][0] is not None and tmp_list or None))
297
298                 self.l.setList(self.list)
299                 self.findBestEvent()
300
301         def getEventRect(self):
302                 rc = self.event_rect
303                 return Rect( rc.left() + (self.instance and self.instance.position().x() or 0), rc.top(), rc.width(), rc.height() )
304
305         def getTimeEpoch(self):
306                 return self.time_epoch
307
308         def getTimeBase(self):
309                 return self.time_base + (self.offs * self.time_epoch * 60)
310
311         def resetOffset(self):
312                 self.offs = 0
313
314 class TimelineText(HTMLComponent, GUIComponent):
315         def __init__(self):
316                 GUIComponent.__init__(self)
317                 self.l = eListboxPythonMultiContent()
318                 self.l.setSelectionClip(eRect(0,0,0,0))
319                 self.l.setItemHeight(25);
320                 self.l.setFont(0, gFont("Regular", 20))
321
322         GUI_WIDGET = eListbox
323
324         def postWidgetCreate(self, instance):
325                 instance.setContent(self.l)
326
327         def setEntries(self, entries):
328                 res = [ None ] # no private data needed
329                 for x in entries:
330                         tm = x[0]
331                         xpos = x[1]
332                         str = strftime("%H:%M", localtime(tm))
333                         res.append((eListboxPythonMultiContent.TYPE_TEXT, xpos-30, 0, 60, 25, 0, RT_HALIGN_CENTER|RT_VALIGN_CENTER, str))
334                 self.l.setList([res])
335
336 config.misc.graph_mepg_prev_time=ConfigClock(default = time())
337 config.misc.graph_mepg_prev_time_period=ConfigInteger(default=120, limits=(60,300))
338
339 class GraphMultiEPG(Screen):
340         def __init__(self, session, services, zapFunc=None, bouquetChangeCB=None):
341                 Screen.__init__(self, session)
342                 self.bouquetChangeCB = bouquetChangeCB
343                 now = time()
344                 tmp = now % 900
345                 self.ask_time = now - tmp
346                 self.closeRecursive = False
347                 self["key_red"] = Button("")
348                 self["key_green"] = Button(_("Add timer"))
349                 self["timeline_text"] = TimelineText()
350                 self["Event"] = Event()
351                 self.time_lines = [ ]
352                 for x in (0,1,2,3,4,5):
353                         pm = Pixmap()
354                         self.time_lines.append(pm)
355                         self["timeline%d"%(x)] = pm
356                 self["timeline_now"] = Pixmap()
357                 self.services = services
358                 self.zapFunc = zapFunc
359
360                 self["list"] = EPGList(selChangedCB = self.onSelectionChanged, timer = self.session.nav.RecordTimer, time_epoch = config.misc.graph_mepg_prev_time_period.value )
361
362                 self["actions"] = ActionMap(["EPGSelectActions", "OkCancelActions"],
363                         {
364                                 "cancel": self.closeScreen,
365                                 "ok": self.eventSelected,
366                                 "timerAdd": self.timerAdd,
367                                 "info": self.infoKeyPressed,
368                                 "red": self.zapTo,
369                                 "input_date_time": self.enterDateTime,
370                                 "nextBouquet": self.nextBouquet,
371                                 "prevBouquet": self.prevBouquet,
372                         })
373                 self["actions"].csel = self
374
375                 self["input_actions"] = ActionMap(["InputActions"],
376                         {
377                                 "left": self.leftPressed,
378                                 "right": self.rightPressed,
379                                 "1": self.key1,
380                                 "2": self.key2,
381                                 "3": self.key3,
382                                 "4": self.key4,
383                                 "5": self.key5,
384                         },-1)
385
386                 self.updateTimelineTimer = eTimer()
387                 self.updateTimelineTimer.callback.append(self.moveTimeLines)
388                 self.updateTimelineTimer.start(60*1000)
389                 self.onLayoutFinish.append(self.onCreate)
390
391         def leftPressed(self):
392                 self.prevEvent()
393
394         def rightPressed(self):
395                 self.nextEvent()
396
397         def nextEvent(self, visible=True):
398                 ret = self["list"].selEntry(+1, visible)
399                 if ret:
400                         self.moveTimeLines(True)
401
402         def prevEvent(self, visible=True):
403                 ret = self["list"].selEntry(-1, visible)
404                 if ret:
405                         self.moveTimeLines(True)
406
407         def key1(self):
408                 self["list"].setEpoch(60)
409                 config.misc.graph_mepg_prev_time_period.value = 60
410                 self.moveTimeLines()
411
412         def key2(self):
413                 self["list"].setEpoch(120)
414                 config.misc.graph_mepg_prev_time_period.value = 120
415                 self.moveTimeLines()
416
417         def key3(self):
418                 self["list"].setEpoch(180)
419                 config.misc.graph_mepg_prev_time_period.value = 180
420                 self.moveTimeLines()
421
422         def key4(self):
423                 self["list"].setEpoch(240)
424                 config.misc.graph_mepg_prev_time_period.value = 240
425                 self.moveTimeLines()
426
427         def key5(self):
428                 self["list"].setEpoch(300)
429                 config.misc.graph_mepg_prev_time_period.value = 300
430                 self.moveTimeLines()
431
432         def nextBouquet(self):
433                 if self.bouquetChangeCB:
434                         self.bouquetChangeCB(1, self)
435
436         def prevBouquet(self):
437                 if self.bouquetChangeCB:
438                         self.bouquetChangeCB(-1, self)
439
440         def enterDateTime(self):
441                 self.session.openWithCallback(self.onDateTimeInputClosed, TimeDateInput, config.misc.graph_mepg_prev_time )
442
443         def onDateTimeInputClosed(self, ret):
444                 if len(ret) > 1:
445                         if ret[0]:
446                                 self.ask_time=ret[1]
447                                 l = self["list"]
448                                 l.resetOffset()
449                                 l.fillMultiEPG(self.services, ret[1])
450                                 self.moveTimeLines(True)
451
452         def closeScreen(self):
453                 self.close(self.closeRecursive)
454
455         def infoKeyPressed(self):
456                 cur = self["list"].getCurrent()
457                 event = cur[0]
458                 service = cur[1]
459                 if event is not None:
460                         self.session.open(EventViewSimple, event, service, self.eventViewCallback, self.openSimilarList)
461
462         def openSimilarList(self, eventid, refstr):
463                 self.session.open(EPGSelection, refstr, None, eventid)
464
465         def setServices(self, services):
466                 self.services = services
467                 self.onCreate()
468
469         #just used in multipeg
470         def onCreate(self):
471                 self["list"].fillMultiEPG(self.services, self.ask_time)
472                 self.moveTimeLines()
473
474         def eventViewCallback(self, setEvent, setService, val):
475                 l = self["list"]
476                 old = l.getCurrent()
477                 if val == -1:
478                         self.prevEvent(False)
479                 elif val == +1:
480                         self.nextEvent(False)
481                 cur = l.getCurrent()
482                 if cur[0] is None and cur[1].ref != old[1].ref:
483                         self.eventViewCallback(setEvent, setService, val)
484                 else:
485                         setService(cur[1])
486                         setEvent(cur[0])
487
488         def zapTo(self):
489                 if self.zapFunc and self["key_red"].getText() == "Zap":
490                         self.closeRecursive = True
491                         ref = self["list"].getCurrent()[1]
492                         if ref:
493                                 self.zapFunc(ref.ref)
494
495         def eventSelected(self):
496                 self.infoKeyPressed()
497
498         def timerAdd(self):
499                 cur = self["list"].getCurrent()
500                 event = cur[0]
501                 serviceref = cur[1]
502                 if event is None:
503                         return
504                 newEntry = RecordTimerEntry(serviceref, checkOldTimers = True, *parseEvent(event))
505                 self.session.openWithCallback(self.finishedAdd, TimerEntry, newEntry)
506
507         def finishedAdd(self, answer):
508                 print "finished add"
509                 if answer[0]:
510                         entry = answer[1]
511                         simulTimerList = self.session.nav.RecordTimer.record(entry)
512                         if simulTimerList is not None:
513                                 if (len(simulTimerList) == 2) and (simulTimerList[1].dontSave) and (simulTimerList[1].autoincrease):
514                                         simulTimerList[1].end = entry.begin - 30
515                                         self.session.nav.RecordTimer.timeChanged(simulTimerList[1])
516                                         self.session.nav.RecordTimer.record(entry)
517                                 else:
518                                         self.session.openWithCallback(self.finishSanityCorrection, TimerSanityConflict, simulTimerList)
519                 else:
520                         print "Timeredit aborted"               
521         
522         def finishSanityCorrection(self, answer):
523                 self.finishedAdd(answer)
524
525         def onSelectionChanged(self):
526                 evt = self["list"].getCurrent()
527                 self["Event"].newEvent(evt and evt[0])
528                 if evt and evt[0]:
529                         evt = evt[0]
530                         now = time()
531                         start = evt.getBeginTime()
532                         end = start + evt.getDuration()
533                         if now >= start and now <= end:
534                                 self["key_red"].setText("Zap")
535                         else:
536                                 self["key_red"].setText("")
537
538         def moveTimeLines(self, force=False):
539                 l = self["list"]
540                 event_rect = l.getEventRect()
541                 time_epoch = l.getTimeEpoch()
542                 time_base = l.getTimeBase()
543                 if event_rect is None or time_epoch is None or time_base is None:
544                         return
545                 time_steps = time_epoch > 180 and 60 or 30
546                 num_lines = time_epoch/time_steps
547                 incWidth=event_rect.width()/num_lines
548                 pos=event_rect.left()
549                 timeline_entries = [ ]
550                 x = 0
551                 changecount = 0
552                 for line in self.time_lines:
553                         old_pos = line.position
554                         new_pos = (x == num_lines and event_rect.left()+event_rect.width() or pos, old_pos[1])
555                         if not x or x >= num_lines:
556                                 line.visible = False
557                         else:
558                                 if old_pos != new_pos:
559                                         line.setPosition(new_pos[0], new_pos[1])
560                                         changecount += 1
561                                 line.visible = True
562                         if not x or line.visible:
563                                 timeline_entries.append((time_base + x * time_steps * 60, new_pos[0]))
564                         x += 1
565                         pos += incWidth
566
567                 if changecount or force:
568                         self["timeline_text"].setEntries(timeline_entries)
569
570                 now=time()
571                 timeline_now = self["timeline_now"]
572                 if now >= time_base and now < (time_base + time_epoch * 60):
573                         bla = (event_rect.width() * 1000) / time_epoch
574                         xpos = ((now/60) - (time_base/60)) * bla / 1000
575                         old_pos = timeline_now.position
576                         new_pos = (xpos+event_rect.left(), old_pos[1])
577                         if old_pos != new_pos:
578                                 timeline_now.setPosition(new_pos[0], new_pos[1])
579                         timeline_now.visible = True
580                 else:
581                         timeline_now.visible = False