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