pipzap: fix possible crash if reusing InfoBar* in unexpected context
[enigma2-plugins.git] / pipzap / src / plugin.py
1 # Plugin definition
2 from Plugins.Plugin import PluginDescriptor
3
4 from Components.ActionMap import HelpableActionMap
5 from Components.ChoiceList import ChoiceEntryComponent
6 from Components.config import config, ConfigSubsection, ConfigEnableDisable
7 from Components.SystemInfo import SystemInfo
8 from Components.ParentalControl import parentalControl
9 from enigma import ePoint, eServiceReference, getDesktop
10 from Screens.ChannelSelection import ChannelContextMenu, ChannelSelection, ChannelSelectionBase
11 from Screens.InfoBar import InfoBar, MoviePlayer
12 from Screens.InfoBarGenerics import InfoBarNumberZap, InfoBarEPG, InfoBarChannelSelection, InfoBarPiP, InfoBarShowMovies, InfoBarTimeshift, InfoBarSeek, InfoBarPlugins
13 from Screens.PictureInPicture import PictureInPicture
14 from Screens.Screen import Screen
15 from Screens.MessageBox import MessageBox
16 from PipzapSetup import PipzapSetup
17 from Components.PluginComponent import plugins
18
19 class baseMethods:
20         pass
21
22 #pragma mark -
23 #pragma mark ChannelSelection
24 #pragma mark -
25
26 # ChannelContextMenu: switch "Activate Picture in Picture" for pip/mainpicture
27 def ChannelContextMenu___init__(self, session, csel, *args, **kwargs):
28         baseMethods.ChannelContextMenu__init__(self, session, csel, *args, **kwargs)
29
30         if self.pipAvailable:
31                 list = self["menu"].list
32                 x = 0
33                 searchText = _("Activate Picture in Picture")
34                 for entry in list:
35                         if entry[0][0] == searchText:
36                                 if csel.dopipzap:
37                                         entry = ChoiceEntryComponent("", (_("play in mainwindow"), self.playMain))
38                                 else:
39                                         entry = ChoiceEntryComponent("blue", (_("play as picture in picture"), self.showServiceInPiP))
40                                 list[x] = entry
41                                 break
42                         x += 1
43                 self["menu"].setList(list)
44
45 def ChannelContextMenu_playMain(self):
46         # XXX: we want to keep the current selection
47         sel = self.csel.getCurrentSelection()
48         self.csel.zap()
49         self.csel.setCurrentSelection(sel)
50         self.close()
51
52 # do not hide existing pip
53 def ChannelContextMenu_showServiceInPiP(self):
54         if not self.pipAvailable:
55                 return
56
57         if not self.session.pipshown:
58                 self.session.pip = self.session.instantiateDialog(PictureInPicture)
59                 self.session.pip.show()
60
61         newservice = self.csel.servicelist.getCurrent()
62         if self.session.pip.playService(newservice):
63                 self.session.pipshown = True
64                 self.session.pip.servicePath = self.csel.getCurrentServicePath()
65                 self.close(True)
66         else:
67                 self.session.pipshown = False
68                 del self.session.pip
69                 self.session.openWithCallback(self.close, MessageBox, _("Could not open Picture in Picture"), MessageBox.TYPE_ERROR)
70
71 def ChannelSelectionBase__init__(self, *args, **kwargs):
72         baseMethods.ChannelSelectionBase__init__(self, *args, **kwargs)
73         self.dopipzap = False
74         self.enable_pipzap = False
75
76 def ChannelSelectionBase_setCurrentSelection(self, service, *args, **kwargs):
77         if service:
78                 baseMethods.ChannelSelectionBase_setCurrentSelection(self, service, *args, **kwargs)
79
80 def ChannelSelection_channelSelected(self, *args, **kwargs):
81         self.enable_pipzap = True
82         baseMethods.ChannelSelection_channelSelected(self, *args, **kwargs)
83
84 def ChannelSelection_togglePipzap(self):
85         assert(self.session.pip)
86         title = self.instance.getTitle()
87         pos = title.find(" (")
88         if pos != -1:
89                 title = title[:pos]
90         if self.dopipzap:
91                 # Mark PiP as inactive and effectively deactivate pipzap
92                 self.session.pip.inactive()
93                 self.dopipzap = False
94
95                 # Disable PiP if not playing a service
96                 if self.session.pip.pipservice is None:
97                         self.session.pipshown = False
98                         del self.session.pip
99
100                 # Move to playing service
101                 lastservice = eServiceReference(self.lastservice.value)
102                 if lastservice.valid() and self.getCurrentSelection() != lastservice:
103                         self.setCurrentSelection(lastservice)
104
105                 title += " (TV)"
106         else:
107                 # Mark PiP as active and effectively active pipzap
108                 self.session.pip.active()
109                 self.dopipzap = True
110
111                 # Move to service playing in pip (will not work with subservices)
112                 self.setCurrentSelection(self.session.pip.getCurrentService())
113
114                 title += " (PiP)"
115         self.setTitle(title)
116         self.buildTitleString()
117
118 def ChannelSelection_zap(self, *args, **kwargs):
119         if self.enable_pipzap and self.dopipzap:
120                 self.revertMode=None
121                 ref = self.session.pip.getCurrentService()
122                 nref = self.getCurrentSelection()
123                 if ref is None or ref != nref:
124                         if not config.ParentalControl.configured.value or parentalControl.getProtectionLevel(nref.toCompareString()) == -1:
125                                 if not self.session.pip.playService(nref):
126                                         # XXX: Make sure we set an invalid ref
127                                         self.session.pip.playService(None)
128         else:
129                 baseMethods.ChannelSelection_zap(self, *args, **kwargs)
130
131                 # Yes, we might double-check this, but we need to re-select pipservice if pipzap is active
132                 # and we just wanted to zap in mainwindow once
133                 # XXX: do we really want this? this also resets the service when zapping from context menu
134                 #      which is irritating
135                 if self.dopipzap:
136                         # This unfortunately won't work with subservices
137                         self.setCurrentSelection(self.session.pip.getCurrentService())
138         self.enable_pipzap = False
139
140 def ChannelSelection_setHistoryPath(self, *args, **kwargs):
141         baseMethods.ChannelSelection_setHistoryPath(self, *args, **kwargs)
142         if self.dopipzap:
143                 self.setCurrentSelection(self.session.pip.getCurrentService())
144
145 def ChannelSelection_cancel(self, *args, **kwargs):
146         if self.revertMode is None and self.dopipzap:
147                 # This unfortunately won't work with subservices
148                 self.setCurrentSelection(self.session.pip.getCurrentService())
149                 self.revertMode = 1337 # not in (None, MODE_TV, MODE_RADIO)
150         baseMethods.ChannelSelection_cancel(self, *args, **kwargs)
151
152 #pragma mark -
153 #pragma mark MoviePlayer
154 #pragma mark -
155
156 def MoviePlayer__init__(self, *args, **kwargs):
157         baseMethods.MoviePlayer__init__(self, *args, **kwargs)
158         self.servicelist = InfoBar.instance and InfoBar.instance.servicelist
159
160         self["DirectionActions"] = HelpableActionMap(self, "DirectionActions",
161                 {
162                         "left": self.left,
163                         "right": self.right
164                 }, prio = -2)
165
166 def MoviePlayer_up(self):
167         slist = self.servicelist
168         if slist and slist.dopipzap:
169                 slist.moveUp()
170                 self.session.execDialog(slist)
171         else:
172                 self.showMovies()
173
174 def MoviePlayer_down(self):
175         slist = self.servicelist
176         if slist and slist.dopipzap:
177                 slist.moveDown()
178                 self.session.execDialog(slist)
179         else:
180                 self.showMovies()
181
182 def MoviePlayer_right(self):
183         # XXX: gross hack, we do not really seek if changing channel in pip :-)
184         slist = self.servicelist
185         if slist and slist.dopipzap:
186                 # XXX: We replicate InfoBarChannelSelection.zapDown here - we shouldn't do that
187                 if slist.inBouquet():
188                         prev = slist.getCurrentSelection()
189                         if prev:
190                                 prev = prev.toString()
191                                 while True:
192                                         if config.usage.quickzap_bouquet_change.value and slist.atEnd():
193                                                 slist.nextBouquet()
194                                         else:
195                                                 slist.moveDown()
196                                         cur = slist.getCurrentSelection()
197                                         if not cur or (not (cur.flags & 64)) or cur.toString() == prev:
198                                                 break
199                 else:
200                         slist.moveDown()
201                 slist.enable_pipzap = True
202                 slist.zap()
203         else:
204                 InfoBarSeek.seekFwd(self)
205
206 def MoviePlayer_left(self):
207         slist = self.servicelist
208         if slist and slist.dopipzap:
209                 # XXX: We replicate InfoBarChannelSelection.zapUp here - we shouldn't do that
210                 if slist.inBouquet():
211                         prev = slist.getCurrentSelection()
212                         if prev:
213                                 prev = prev.toString()
214                                 while True:
215                                         if config.usage.quickzap_bouquet_change.value:
216                                                 if slist.atBegin():
217                                                         slist.prevBouquet()
218                                         slist.moveUp()
219                                         cur = slist.getCurrentSelection()
220                                         if not cur or (not (cur.flags & 64)) or cur.toString() == prev:
221                                                 break
222                 else:
223                         slist.moveUp()
224                 slist.enable_pipzap = True
225                 slist.zap()
226         else:
227                 InfoBarSeek.seekBack(self)
228
229 def MoviePlayer_swapPiP(self):
230         pass
231
232 #pragma mark -
233 #pragma mark InfoBarGenerics
234 #pragma mark -
235
236 def InfoBarNumberZap_zapToNumber(self, *args, **kwargs):
237         try:
238                 self.servicelist.enable_pipzap = True
239         except AttributeError, ae:
240                 pass
241         baseMethods.InfoBarNumberZap_zapToNumber(self, *args, **kwargs)
242
243 def InfoBarChannelSelection_zapUp(self, *args, **kwargs):
244         self.servicelist.enable_pipzap = True
245         baseMethods.InfoBarChannelSelection_zapUp(self, *args, **kwargs)
246
247 def InfoBarChannelSelection_zapDown(self, *args, **kwargs):
248         self.servicelist.enable_pipzap = True
249         baseMethods.InfoBarChannelSelection_zapDown(self, *args, **kwargs)
250
251 def InfoBarEPG_zapToService(self, *args, **kwargs):
252         try:
253                 self.servicelist.enable_pipzap = True
254         except AttributeError, ae:
255                 pass
256         baseMethods.InfoBarEPG_zapToService(self, *args, **kwargs)
257
258 def InfoBarShowMovies__init__(self):
259         baseMethods.InfoBarShowMovies__init__(self)
260         self["MovieListActions"] = HelpableActionMap(self, "InfobarMovieListActions",
261                 {
262                         "movieList": (self.showMovies, _("movie list")),
263                         "up": (self.up, _("movie list")),
264                         "down": (self.down, _("movie list"))
265                 })
266
267 def InfoBarPiP__init__(self):
268         baseMethods.InfoBarPiP__init__(self)
269         if SystemInfo.get("NumVideoDecoders", 1) > 1 and self.allowPiP:
270                 self.addExtension((self.getTogglePipzapName, self.togglePipzap, self.pipzapAvailable), "red")
271                 if config.plugins.pipzap.enable_hotkey.value:
272                         self["pipzapActions"] = HelpableActionMap(self, "pipzapActions",
273                                 {
274                                         "switchPiP": (self.togglePipzap, _("zap in pip window...")),
275                                 })
276
277 def InfoBarPiP_pipzapAvailable(self):
278         try:
279                 return True if self.servicelist and self.session.pipshown else False
280         except AttributeError, ae:
281                 return False
282
283 def InfoBarPiP_getTogglePipzapName(self):
284         slist = self.servicelist
285         if slist and slist.dopipzap:
286                 return _("Zap focus to main screen")
287         return _("Zap focus to Picture in Picture")
288
289 def InfoBarPiP_togglePipzap(self):
290         # supposed to fix some problems with permanent timeshift patch
291         if isinstance(self, InfoBarTimeshift) and isinstance(self, InfoBarSeek) and \
292                 self.timeshift_enabled and self.isSeekable():
293                         return 0
294
295         if not self.session.pipshown:
296                 self.showPiP()
297         slist = self.servicelist
298         if slist:
299                 slist.togglePipzap()
300
301 def InfoBarPiP_showPiP(self, *args, **kwargs):
302         try:
303                 slist = self.servicelist
304         except AttributeError, ae:
305                 slist = None
306
307         if self.session.pipshown and slist and slist.dopipzap:
308                 slist.togglePipzap()
309         elif not self.session.pipshown and isinstance(self, InfoBarShowMovies):
310                 self.session.pip = self.session.instantiateDialog(PictureInPicture)
311                 self.session.pip.show()
312                 self.session.pipshown = True
313                 if slist:
314                         self.session.pip.playService(slist.getCurrentSelection())
315                 return
316         baseMethods.InfoBarPiP_showPiP(self, *args, **kwargs)
317
318 # Using the base implementation would cause nasty bugs, so ignore it here
319 def InfoBarPiP_swapPiP(self):
320         swapservice = self.session.nav.getCurrentlyPlayingServiceReference()
321         pipref = self.session.pip.getCurrentService()
322         if pipref and swapservice and pipref.toString() != swapservice.toString():
323                         self.session.pip.playService(swapservice)
324
325                         try:
326                                 slist = self.servicelist
327                         except AttributeError, ae:
328                                 slist = None
329                         if slist:
330                                 # TODO: this behaves real bad on subservices
331                                 if slist.dopipzap:
332                                         slist.servicelist.setCurrent(swapservice)
333                                 else:
334                                         slist.servicelist.setCurrent(pipref)
335
336                                 slist.addToHistory(pipref) # add service to history
337                                 slist.lastservice.value = pipref.toString() # save service as last playing one
338                         self.session.nav.stopService() # stop portal
339                         self.session.nav.playService(pipref) # start subservice
340
341 #pragma mark -
342 #pragma mark Picture in Picture
343 #pragma mark -
344
345 class PictureInPictureZapping(Screen):
346         skin = """<screen name="PictureInPictureZapping" flags="wfNoBorder" position="50,50" size="90,26" title="PiPZap" zPosition="-1">
347                 <eLabel text="PiP-Zap" position="0,0" size="90,26" foregroundColor="#00ff66" font="Regular;26" />
348         </screen>"""
349         def refreshPosition(self):
350                 x = config.av.pip.value[0]
351                 y = config.av.pip.value[1]
352                 width = getDesktop(0).size().width()
353                 height = getDesktop(0).size().height()
354                 if x > width / 2:
355                         x = 40
356                 else:
357                         x = width - 120
358                 if y > height / 2:
359                         y = 40
360                 else:
361                         y = height - 55
362                 self.instance.move(ePoint(x, y))
363
364 def PictureInPicture__init__(self, session, *args, **kwargs):
365         baseMethods.PictureInPicture__init__(self, session, *args, **kwargs)
366         self.pipActive = session.instantiateDialog(PictureInPictureZapping)
367
368 def PictureInPicture_active(self):
369         if config.plugins.pipzap.show_label.value:
370                 self.pipActive.show()
371
372 def PictureInPicture_inactive(self):
373         self.pipActive.hide()
374
375 def PictureInPicture_move(self, *args, **kwargs):
376         baseMethods.PictureInPicture_move(self, *args, **kwargs)
377         self.pipActive.refreshPosition()
378
379 #pragma mark -
380 #pragma mark Plugin
381 #pragma mark -
382
383 def overwriteFunctions():
384         """Overwrite existing functions here to increase system stability a bit."""
385         baseMethods.ChannelContextMenu__init__ = ChannelContextMenu.__init__
386         ChannelContextMenu.__init__ = ChannelContextMenu___init__
387
388         ChannelContextMenu.playMain = ChannelContextMenu_playMain
389         ChannelContextMenu.showServiceInPiP = ChannelContextMenu_showServiceInPiP
390
391         baseMethods.ChannelSelectionBase__init__ = ChannelSelectionBase.__init__
392         ChannelSelectionBase.__init__ = ChannelSelectionBase__init__
393
394         baseMethods.ChannelSelectionBase_setCurrentSelection = ChannelSelectionBase.setCurrentSelection
395         ChannelSelectionBase.setCurrentSelection = ChannelSelectionBase_setCurrentSelection
396
397         baseMethods.ChannelSelection_channelSelected = ChannelSelection.channelSelected
398         ChannelSelection.channelSelected = ChannelSelection_channelSelected
399
400         ChannelSelection.togglePipzap = ChannelSelection_togglePipzap 
401
402         baseMethods.ChannelSelection_zap = ChannelSelection.zap
403         ChannelSelection.zap = ChannelSelection_zap
404
405         baseMethods.ChannelSelection_setHistoryPath = ChannelSelection.setHistoryPath
406         ChannelSelection.setHistoryPath = ChannelSelection_setHistoryPath
407
408         baseMethods.ChannelSelection_cancel = ChannelSelection.cancel
409         ChannelSelection.cancel = ChannelSelection_cancel
410
411         baseMethods.MoviePlayer__init__ = MoviePlayer.__init__
412         MoviePlayer.__init__ = MoviePlayer__init__
413
414         MoviePlayer.allowPiP = property(lambda *x: True, lambda *x: None)
415
416         MoviePlayer.up = MoviePlayer_up
417         MoviePlayer.down = MoviePlayer_down
418         MoviePlayer.right = MoviePlayer_right
419         MoviePlayer.left = MoviePlayer_left
420         MoviePlayer.swapPiP = MoviePlayer_swapPiP
421
422         baseMethods.InfoBarNumberZap_zapToNumber = InfoBarNumberZap.zapToNumber
423         InfoBarNumberZap.zapToNumber = InfoBarNumberZap_zapToNumber
424
425         baseMethods.InfoBarChannelSelection_zapUp = InfoBarChannelSelection.zapUp
426         InfoBarChannelSelection.zapUp = InfoBarChannelSelection_zapUp
427
428         baseMethods.InfoBarChannelSelection_zapDown = InfoBarChannelSelection.zapDown
429         InfoBarChannelSelection.zapDown = InfoBarChannelSelection_zapDown
430
431         baseMethods.InfoBarEPG_zapToService = InfoBarEPG.zapToService
432         InfoBarEPG.zapToService = InfoBarEPG_zapToService
433
434         baseMethods.InfoBarShowMovies__init__ = InfoBarShowMovies.__init__
435         InfoBarShowMovies.__init__ = InfoBarShowMovies__init__
436
437         baseMethods.InfoBarPiP__init__ = InfoBarPiP.__init__
438         InfoBarPiP.__init__ = InfoBarPiP__init__
439
440         InfoBarPiP.pipzapAvailable = InfoBarPiP_pipzapAvailable
441         InfoBarPiP.getTogglePipzapName = InfoBarPiP_getTogglePipzapName
442         InfoBarPiP.togglePipzap = InfoBarPiP_togglePipzap
443         InfoBarPiP.swapPiP = InfoBarPiP_swapPiP
444
445         baseMethods.InfoBarPiP_showPiP = InfoBarPiP.showPiP
446         InfoBarPiP.showPiP = InfoBarPiP_showPiP
447
448         baseMethods.PictureInPicture__init__ = PictureInPicture.__init__
449         PictureInPicture.__init__ = PictureInPicture__init__
450
451         PictureInPicture.active = PictureInPicture_active
452         PictureInPicture.inactive = PictureInPicture_inactive
453
454         baseMethods.PictureInPicture_move = PictureInPicture.move
455         PictureInPicture.move = PictureInPicture_move
456
457 config.plugins.pipzap = ConfigSubsection()
458 config.plugins.pipzap.enable_hotkey = ConfigEnableDisable(default = True)
459 config.plugins.pipzap.show_in_plugins = ConfigEnableDisable(default = False)
460 config.plugins.pipzap.show_label = ConfigEnableDisable(default = True)
461
462 def autostart(reason, **kwargs):
463         if reason == 0:
464                 overwriteFunctions()
465
466 def activate(session, *args, **kwargs):
467         infobar = InfoBar.instance
468         if not infobar:
469                 session.open(MessageBox, _("Unable to access InfoBar.\npipzap not available."), MessageBox.TYPE_ERROR)
470         elif hasattr(infobar, 'togglePipzap'): # check if plugin is already hooked into enigma2
471                 infobar.togglePipzap()
472         else:
473                 session.open(MessageBox, _("pipzap not properly installed.\nPlease restart Enigma2."), MessageBox.TYPE_ERROR)
474
475 def main(session, *args, **kwargs):
476         session.open(PipzapSetup)
477
478 def menu(menuid):
479         if menuid != "system":
480                 return []
481         return [(_("pipzap"), main, "pipzap_setup", None)]
482
483 def housekeepingPluginmenu(el):
484         if el.value:
485                 plugins.addPlugin(activateDescriptor)
486         else:
487                 plugins.removePlugin(activateDescriptor)
488
489 config.plugins.pipzap.show_in_plugins.addNotifier(housekeepingPluginmenu, initial_call=False, immediate_feedback=True)
490 activateDescriptor = PluginDescriptor(name="pipzap", description=_("Toggle pipzap status"), where=PluginDescriptor.WHERE_PLUGINMENU, fnc=activate, needsRestart=False)
491
492 def showHideNotifier(el):
493         infobar = InfoBar.instance
494         if not infobar:
495                 return
496         session = infobar.session
497         slist = infobar.servicelist
498         if slist and hasattr(slist, 'dopipzap'): # check if plugin is already hooked into enigma2
499                 if session.pipshown:
500                         if el.value and slist.dopipzap:
501                                 session.pip.active()
502                         else:
503                                 session.pip.inactive()
504
505 config.plugins.pipzap.show_label.addNotifier(showHideNotifier, initial_call=False, immediate_feedback=True)
506
507 def Plugins(**kwargs):
508         # do not add any entry if only one (or less :P) video decoders present
509         if SystemInfo.get("NumVideoDecoders", 1) < 2:
510                 return []
511
512         l = [
513                 PluginDescriptor(
514                         where=PluginDescriptor.WHERE_AUTOSTART,
515                         fnc=autostart,
516                         needsRestart=True, # XXX: force restart for now as I don't think the plugin will work properly without one
517                 ),
518                 PluginDescriptor(
519                         where=PluginDescriptor.WHERE_MENU,
520                         fnc=menu,
521                         needsRestart=False,
522                 ),
523         ]
524         if config.plugins.pipzap.show_in_plugins.value:
525                 l.append(activateDescriptor)
526         return l