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