Merge branch 'bug_352_fix_some_cutlist_bugs'
[enigma2.git] / lib / python / Plugins / Extensions / CutListEditor / plugin.py
index d3573743070af3ddf489c4fc4fa74d522f920612..abd606d6fa2d9e1bd15580507f485e7f29b3cfee 100644 (file)
@@ -4,18 +4,21 @@ from Screens.Screen import Screen
 from Screens.MessageBox import MessageBox
 from Components.ServicePosition import ServicePositionGauge
 from Components.ActionMap import HelpableActionMap
-from Components.MenuList import MenuList
-from Components.MultiContent import MultiContentEntryText, RT_HALIGN_RIGHT
-from Components.ServiceEventTracker import ServiceEventTracker
-
+from Components.MultiContent import MultiContentEntryText
+from Components.ServiceEventTracker import ServiceEventTracker, InfoBarBase
+from Components.VideoWindow import VideoWindow
+from Components.Label import Label
 from Screens.InfoBarGenerics import InfoBarSeek, InfoBarCueSheetSupport
-
 from Components.GUIComponent import GUIComponent
+from enigma import eListboxPythonMultiContent, eListbox, gFont, iPlayableService, RT_HALIGN_RIGHT
+from Screens.FixedMenu import FixedMenu
+from Screens.HelpMenu import HelpableScreen
+from ServiceReference import ServiceReference
+from Components.Sources.List import List
 
-from enigma import eListboxPythonMultiContent, eListbox, gFont, iPlayableService
+import bisect
 
 def CutListEntry(where, what):
-       res = [ (where, what) ]
        w = where / 90
        ms = w % 1000
        s = (w / 1000) % 60
@@ -23,124 +26,217 @@ def CutListEntry(where, what):
        h = w / 3600000
        if what == 0:
                type = "IN"
+               type_col = 0x004000
        elif what == 1:
                type = "OUT"
+               type_col = 0x400000
        elif what == 2:
                type = "MARK"
-       res.append(MultiContentEntryText(size=(400, 20), text = "%dh:%02dm:%02ds:%03d" % (h, m, s, ms)))
-       res.append(MultiContentEntryText(pos=(400,0), size=(130, 20), text = type, flags = RT_HALIGN_RIGHT))
-
-       return res
-
-class CutList(GUIComponent):
-       def __init__(self, list):
-               GUIComponent.__init__(self)
-               self.l = eListboxPythonMultiContent()
-               self.setList(list)
-               self.l.setFont(0, gFont("Regular", 20))
-               self.onSelectionChanged = [ ]
-       
-       def getCurrent(self):
-               return self.l.getCurrentSelection()
-       
-       def getCurrentIndex(self):
-               return self.l.getCurrentSelectionIndex()
-       
-       def GUIcreate(self, parent):
-               self.instance = eListbox(parent)
-               self.instance.setContent(self.l)
-               self.instance.setItemHeight(30)
-               self.instance.selectionChanged.get().append(self.selectionChanged)
+               type_col = 0x000040
+       elif what == 3:
+               type = "LAST"
+               type_col = 0x000000
+       return ((where, what), "%dh:%02dm:%02ds:%03d" % (h, m, s, ms), type, type_col)
 
-       def selectionChanged(self):
-               for x in self.onSelectionChanged:
-                       x()
-       
-       def GUIdelete(self):
-               self.instance.selectionChanged.get().remove(self.selectionChanged)
-               self.instance.setContent(None)
-               self.instance = None
-       
-       def invalidateEntry(self, index):
-               self.l.invalidateEntry(index)
-       
-       def setIndex(self, index, data):
-               self.list[index] = data
-               self.invalidateEntry(index)
-
-       def setList(self, list):
-               self.list = list
-               self.l.setList(self.list)
-
-class CutListEditor(Screen, InfoBarSeek, InfoBarCueSheetSupport):
+class CutListContextMenu(FixedMenu):
+       RET_STARTCUT = 0
+       RET_ENDCUT = 1
+       RET_DELETECUT = 2
+       RET_MARK = 3
+       RET_DELETEMARK = 4
+       RET_REMOVEBEFORE = 5
+       RET_REMOVEAFTER = 6
+       RET_GRABFRAME = 7
+
+       SHOW_STARTCUT = 0
+       SHOW_ENDCUT = 1
+       SHOW_DELETECUT = 2
+
+       def __init__(self, session, state, nearmark):
+               menu = [(_("back"), self.close)] #, (None, )]
+
+               if state == self.SHOW_STARTCUT:
+                       menu.append((_("start cut here"), self.startCut))
+               else:
+                       menu.append((_("start cut here"), ))
+
+               if state == self.SHOW_ENDCUT:
+                       menu.append((_("end cut here"), self.endCut))
+               else:
+                       menu.append((_("end cut here"), ))
+
+               if state == self.SHOW_DELETECUT:
+                       menu.append((_("delete cut"), self.deleteCut))
+               else:
+                       menu.append((_("delete cut"), ))
+
+               menu.append((_("remove before this position"), self.removeBefore))
+               menu.append((_("remove after this position"), self.removeAfter))
+
+#              menu.append((None, ))
+
+               if not nearmark:
+                       menu.append((_("insert mark here"), self.insertMark))
+               else:
+                       menu.append((_("remove this mark"), self.removeMark))
+
+               menu.append((_("grab this frame as bitmap"), self.grabFrame))
+               FixedMenu.__init__(self, session, _("Cut"), menu)
+               self.skinName = "Menu"
+
+       def startCut(self):
+               self.close(self.RET_STARTCUT)
+
+       def endCut(self):
+               self.close(self.RET_ENDCUT)
+
+       def deleteCut(self):
+               self.close(self.RET_DELETECUT)
+
+       def insertMark(self):
+               self.close(self.RET_MARK)
+
+       def removeMark(self):
+               self.close(self.RET_DELETEMARK)
+
+       def removeBefore(self):
+               self.close(self.RET_REMOVEBEFORE)
+
+       def removeAfter(self):
+               self.close(self.RET_REMOVEAFTER)
+
+       def grabFrame(self):
+               self.close(self.RET_GRABFRAME)
+
+class CutListEditor(Screen, InfoBarBase, InfoBarSeek, InfoBarCueSheetSupport, HelpableScreen):
        skin = """
-               <screen position="100,100" size="550,400" title="Test" >
-                       <widget name="Timeline" position="10,0" size="530,40" 
-                               pointer="/usr/share/enigma2/position_pointer.png:3,5" />
-                       <widget name="Cutlist" position="10,50" size="530,200" />
-               </screen>"""
+       <screen position="0,0" size="720,576" title="Cutlist editor" flags="wfNoBorder">
+               <eLabel text="Cutlist editor" position="65,60" size="300,25" font="Regular;20" />
+               <widget source="global.CurrentTime" render="Label" position="268,60" size="394,20" font="Regular;20" halign="right">
+                       <convert type="ClockToText">Format:%A %B %d, %H:%M</convert>
+               </widget>
+               <eLabel position="268,98" size="394,304" backgroundColor="#505555" />
+               <widget name="Video" position="270,100" zPosition="1" size="390,300" backgroundColor="transparent" />
+               <widget source="session.CurrentService" render="Label" position="135,405" size="450,50" font="Regular;22" halign="center" valign="center">
+                       <convert type="ServiceName">Name</convert>
+               </widget>
+               <widget source="session.CurrentService" render="Label" position="320,450" zPosition="1" size="420,25" font="Regular;20" halign="left" valign="center">
+                       <convert type="ServicePosition">Position,Detailed</convert>
+               </widget>
+               <widget name="SeekState" position="210,450" zPosition="1" size="100,25" halign="right" font="Regular;20" valign="center" />
+               <eLabel position="48,98" size="204,274" backgroundColor="#505555" />
+               <eLabel position="50,100" size="200,270" backgroundColor="#000000" />
+               <widget source="cutlist" position="50,100" zPosition="1" size="200,270" scrollbarMode="showOnDemand" transparent="1" render="Listbox" >
+                       <convert type="TemplatedMultiContent">
+                               {"template": [
+                                               MultiContentEntryText(size=(125, 20), text = 1, backcolor = MultiContentTemplateColor(3)),
+                                               MultiContentEntryText(pos=(125,0), size=(50, 20), text = 2, flags = RT_HALIGN_RIGHT, backcolor = MultiContentTemplateColor(3))
+                                       ],
+                                "fonts": [gFont("Regular", 18)],
+                                "itemHeight": 20
+                               }
+                       </convert>
+               </widget>
+               <widget name="Timeline" position="50,485" size="615,20" backgroundColor="#505555" pointer="skin_default/position_arrow.png:3,5" foregroundColor="black" />
+               <ePixmap pixmap="skin_default/icons/mp_buttons.png" position="305,515" size="109,13" alphatest="on" />
+       </screen>"""
+
        def __init__(self, session, service):
                self.skin = CutListEditor.skin
                Screen.__init__(self, session)
-               InfoBarSeek.__init__(self)
+               InfoBarSeek.__init__(self, actionmap = "CutlistSeekActions")
                InfoBarCueSheetSupport.__init__(self)
+               InfoBarBase.__init__(self, steal_current_service = True)
+               HelpableScreen.__init__(self)
+               self.old_service = session.nav.getCurrentlyPlayingServiceReference()
                session.nav.playService(service)
-               
+
+               service = session.nav.getCurrentService()
+               cue = service and service.cueSheet()
+               if cue is not None:
+                       # disable cutlists. we want to freely browse around in the movie
+                       print "cut lists disabled!"
+                       cue.setCutListEnable(0)
+
                self.downloadCuesheet()
-       
+
                self["Timeline"] = ServicePositionGauge(self.session.nav)
-               self["Cutlist"] = CutList(self.getCutlist())
-               self["Cutlist"].onSelectionChanged.append(self.selectionChanged)
-               
+               self["cutlist"] = List(self.getCutlist())
+               self["cutlist"].onSelectionChanged.append(self.selectionChanged)
+               self["SeekState"] = Label()
+               self.onPlayStateChanged.append(self.updateStateLabel)
+               self.updateStateLabel(self.seekstate)
+
+               self["Video"] = VideoWindow(decoder = 0)
+
                self["actions"] = HelpableActionMap(self, "CutListEditorActions",
                        {
                                "setIn": (self.setIn, _("Make this mark an 'in' point")),
                                "setOut": (self.setOut, _("Make this mark an 'out' point")),
                                "setMark": (self.setMark, _("Make this mark just a mark")),
-                               "leave": (self.exit, _("Exit editor"))
-                       })
-               
+                               "addMark": (self.__addMark, _("Add a mark")),
+                               "removeMark": (self.__removeMark, _("Remove a mark")),
+                               "leave": (self.exit, _("Exit editor")),
+                               "showMenu": (self.showMenu, _("menu")),
+                       }, prio=-4)
+
                self.tutorial_seen = False
-               
+
                self.onExecBegin.append(self.showTutorial)
                self.__event_tracker = ServiceEventTracker(screen=self, eventmap=
                        {
                                iPlayableService.evCuesheetChanged: self.refillList
                        })
-               
+
+               # to track new entries we save the last version of the cutlist
+               self.last_cuts = self.getCutlist()
+               self.cut_start = None
+               self.inhibit_seek = False
+               self.onClose.append(self.__onClose)
+
+       def __onClose(self):
+               self.session.nav.playService(self.old_service, forceRestart=True)
+
+       def updateStateLabel(self, state):
+               self["SeekState"].setText(state[3].strip())
+
        def showTutorial(self):
                if not self.tutorial_seen:
                        self.tutorial_seen = True
-                       self.session.open(MessageBox, 
-                               """Welcome to the Cutlist editor. It has a *very* unintuitive handling:
-
-You can add use the color keys to move around in the recorded movie. 
-By pressing shift-yellow, you can add a mark or remove an existing one.
-You can then assign them to be either 'in' or 'out' positions by selecting them in the list and pressing 1 or 2.
-                               """, MessageBox.TYPE_INFO)
-       
+                       self.session.open(MessageBox,_("Welcome to the Cutlist editor.\n\nSeek to the start of the stuff you want to cut away. Press OK, select 'start cut'.\n\nThen seek to the end, press OK, select 'end cut'. That's it."), MessageBox.TYPE_INFO)
+
        def checkSkipShowHideLock(self):
                pass
-       
+
        def setType(self, index, type):
-               self.cut_list[index] = (self.cut_list[index][0], type)
-               self["Cutlist"].setIndex(index, CutListEntry(*self.cut_list[index]))
-       
+               if len(self.cut_list):
+                       self.cut_list[index] = (self.cut_list[index][0], type)
+                       self["cutlist"].modifyEntry(index, CutListEntry(*self.cut_list[index]))
+
        def setIn(self):
-               m = self["Cutlist"].getCurrentIndex()
+               m = self["cutlist"].getIndex()
                self.setType(m, 0)
                self.uploadCuesheet()
-       
+
        def setOut(self):
-               m = self["Cutlist"].getCurrentIndex()
+               m = self["cutlist"].getIndex()
                self.setType(m, 1)
                self.uploadCuesheet()
 
        def setMark(self):
-               m = self["Cutlist"].getCurrentIndex()
+               m = self["cutlist"].getIndex()
                self.setType(m, 2)
                self.uploadCuesheet()
-       
+
+       def __addMark(self):
+               self.toggleMark(onlyadd=True, tolerance=90000) # do not allow two marks in <1s
+
+       def __removeMark(self):
+               m = self["cutlist"].getCurrent()
+               m = m and m[0]
+               if m is not None:
+                       self.removeMark(m)
+
        def exit(self):
                self.close()
 
@@ -151,24 +247,162 @@ You can then assign them to be either 'in' or 'out' positions by selecting them
                return r
 
        def selectionChanged(self):
-               where = self["Cutlist"].getCurrent()
-               if where is None:
-                       print "no selection"
-                       return
-               pts = where[0][0]
-               seek = self.getSeek()
-               if seek is None:
-                       print "no seek"
-                       return
-               seek.seekTo(pts)
+               if not self.inhibit_seek:
+                       where = self["cutlist"].getCurrent()
+                       if where is None:
+                               print "no selection"
+                               return
+                       pts = where[0][0]
+                       seek = self.getSeek()
+                       if seek is None:
+                               print "no seek"
+                               return
+                       seek.seekTo(pts)
 
        def refillList(self):
                print "cue sheet changed, refilling"
                self.downloadCuesheet()
-               self["Cutlist"].setList(self.getCutlist())
 
-def main(session, service):
+               # get the first changed entry, counted from the end, and select it
+               new_list = self.getCutlist()
+               self["cutlist"].list = new_list
+
+               l1 = len(new_list)
+               l2 = len(self.last_cuts)
+               for i in range(min(l1, l2)):
+                       if new_list[l1-i-1] != self.last_cuts[l2-i-1]:
+                               self["cutlist"].setIndex(l1-i-1)
+                               break
+               self.last_cuts = new_list
+
+       def getStateForPosition(self, pos):
+               state = -1
+               for (where, what) in self.cut_list:
+                       if what in [0, 1]:
+                               if where < pos:
+                                       state = what
+                               elif where == pos:
+                                       state = 1
+                               elif state == -1:
+                                       state = 1 - what
+               if state == -1:
+                       state = 0
+               return state
+
+       def showMenu(self):
+               curpos = self.cueGetCurrentPosition()
+               if curpos is None:
+                       return
+
+               self.setSeekState(self.SEEK_STATE_PAUSE)
+
+               self.context_position = curpos
+
+               self.context_nearest_mark = self.toggleMark(onlyreturn=True)
+
+               cur_state = self.getStateForPosition(curpos)
+               if cur_state == 0:
+                       print "currently in 'IN'"
+                       if self.cut_start is None or self.context_position < self.cut_start:
+                               state = CutListContextMenu.SHOW_STARTCUT
+                       else:
+                               state = CutListContextMenu.SHOW_ENDCUT
+               else:
+                       print "currently in 'OUT'"
+                       state = CutListContextMenu.SHOW_DELETECUT
+
+               if self.context_nearest_mark is None:
+                       nearmark = False
+               else:
+                       nearmark = True
+
+               self.session.openWithCallback(self.menuCallback, CutListContextMenu, state, nearmark)
+
+       def menuCallback(self, *result):
+               if not len(result):
+                       return
+               result = result[0]
+
+               if result == CutListContextMenu.RET_STARTCUT:
+                       self.cut_start = self.context_position
+               elif result == CutListContextMenu.RET_ENDCUT:
+                       # remove in/out marks between the new cut
+                       for (where, what) in self.cut_list[:]:
+                               if self.cut_start <= where <= self.context_position and what in (0,1):
+                                       self.cut_list.remove((where, what))
+
+                       bisect.insort(self.cut_list, (self.cut_start, 1))
+                       bisect.insort(self.cut_list, (self.context_position, 0))
+                       self.uploadCuesheet()
+                       self.cut_start = None
+               elif result == CutListContextMenu.RET_DELETECUT:
+                       out_before = None
+                       in_after = None
+
+                       for (where, what) in self.cut_list:
+                               if what == 1 and where <= self.context_position: # out
+                                       out_before = (where, what)
+                               elif what == 0 and where < self.context_position: # in, before out
+                                       out_before = None
+                               elif what == 0 and where >= self.context_position and in_after is None:
+                                       in_after = (where, what)
+
+                       if out_before is not None:
+                               self.cut_list.remove(out_before)
+
+                       if in_after is not None:
+                               self.cut_list.remove(in_after)
+                       self.inhibit_seek = True
+                       self.uploadCuesheet()
+                       self.inhibit_seek = False
+               elif result == CutListContextMenu.RET_MARK:
+                       self.__addMark()
+               elif result == CutListContextMenu.RET_DELETEMARK:
+                       self.cut_list.remove(self.context_nearest_mark)
+                       self.inhibit_seek = True
+                       self.uploadCuesheet()
+                       self.inhibit_seek = False
+               elif result == CutListContextMenu.RET_REMOVEBEFORE:
+                       # remove in/out marks before current position
+                       for (where, what) in self.cut_list[:]:
+                               if where <= self.context_position and what in (0,1):
+                                       self.cut_list.remove((where, what))
+                       # add 'in' point
+                       bisect.insort(self.cut_list, (self.context_position, 0))
+                       self.inhibit_seek = True
+                       self.uploadCuesheet()
+                       self.inhibit_seek = False
+               elif result == CutListContextMenu.RET_REMOVEAFTER:
+                       # remove in/out marks after current position
+                       for (where, what) in self.cut_list[:]:
+                               if where >= self.context_position and what in (0,1):
+                                       self.cut_list.remove((where, what))
+                       # add 'out' point
+                       bisect.insort(self.cut_list, (self.context_position, 1))
+                       self.inhibit_seek = True
+                       self.uploadCuesheet()
+                       self.inhibit_seek = False
+               elif result == CutListContextMenu.RET_GRABFRAME:
+                       self.grabFrame()
+
+       # we modify the "play" behavior a bit:
+       # if we press pause while being in slowmotion, we will pause (and not play)
+       def playpauseService(self):
+               if self.seekstate != self.SEEK_STATE_PLAY and not self.isStateSlowMotion(self.seekstate):
+                       self.unPauseService()
+               else:
+                       self.pauseService()
+
+       def grabFrame(self):
+               path = self.session.nav.getCurrentlyPlayingServiceReference().getPath()
+               from Components.Console import Console
+               grabConsole = Console()
+               cmd = 'grab -vblpr%d "%s"' % (180, path.rsplit('.',1)[0] + ".png")
+               grabConsole.ePopen(cmd)
+               self.playpauseService()
+
+def main(session, service, **kwargs):
        session.open(CutListEditor, service)
 
-def Plugins():
+def Plugins(**kwargs):
        return PluginDescriptor(name="Cutlist Editor", description=_("Cutlist editor..."), where = PluginDescriptor.WHERE_MOVIELIST, fnc=main)