fix frequently vacancies in multiepg
[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                 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, 'RnITBD')
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.timeout.get().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