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