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