allow burning DVDs with multiple titles where playback automatically jumps to the...
[enigma2.git] / lib / python / Plugins / Extensions / CutListEditor / plugin.py
1 from Plugins.Plugin import PluginDescriptor
2
3 from Screens.Screen import Screen
4 from Screens.MessageBox import MessageBox
5 from Components.ServicePosition import ServicePositionGauge
6 from Components.ActionMap import HelpableActionMap
7 from Components.MultiContent import MultiContentEntryText
8 from Components.ServiceEventTracker import ServiceEventTracker, InfoBarBase
9 from Components.VideoWindow import VideoWindow
10 from Screens.InfoBarGenerics import InfoBarSeek, InfoBarCueSheetSupport
11 from Components.GUIComponent import GUIComponent
12 from enigma import eListboxPythonMultiContent, eListbox, gFont, iPlayableService, RT_HALIGN_RIGHT
13 from Screens.FixedMenu import FixedMenu
14 from Screens.HelpMenu import HelpableScreen
15 import bisect
16
17 def CutListEntry(where, what):
18         res = [ (where, what) ]
19         w = where / 90
20         ms = w % 1000
21         s = (w / 1000) % 60
22         m = (w / 60000) % 60
23         h = w / 3600000
24         if what == 0:
25                 type = "IN"
26         elif what == 1:
27                 type = "OUT"
28         elif what == 2:
29                 type = "MARK"
30         elif what == 3:
31                 type = "LAST"
32         res.append(MultiContentEntryText(size=(400, 20), text = "%dh:%02dm:%02ds:%03d" % (h, m, s, ms)))
33         res.append(MultiContentEntryText(pos=(400,0), size=(130, 20), text = type, flags = RT_HALIGN_RIGHT))
34
35         return res
36
37 class CutListContextMenu(FixedMenu):
38         RET_STARTCUT = 0
39         RET_ENDCUT = 1
40         RET_DELETECUT = 2
41         RET_MARK = 3
42         RET_DELETEMARK = 4
43         RET_REMOVEBEFORE = 5
44         RET_REMOVEAFTER = 6
45
46         SHOW_STARTCUT = 0
47         SHOW_ENDCUT = 1
48         SHOW_DELETECUT = 2
49
50         def __init__(self, session, state, nearmark):
51                 menu = [(_("back"), self.close)] #, (None, )]
52
53                 if state == self.SHOW_STARTCUT:
54                         menu.append((_("start cut here"), self.startCut))
55                 else:
56                         menu.append((_("start cut here"), ))
57
58                 if state == self.SHOW_ENDCUT:
59                         menu.append((_("end cut here"), self.endCut))
60                 else:
61                         menu.append((_("end cut here"), ))
62
63                 if state == self.SHOW_DELETECUT:
64                         menu.append((_("delete cut"), self.deleteCut))
65                 else:
66                         menu.append((_("delete cut"), ))
67
68                 menu.append((_("remove before this position"), self.removeBefore))
69                 menu.append((_("remove after this position"), self.removeAfter))
70
71 #               menu.append((None, ))
72
73                 if not nearmark:
74                         menu.append((_("insert mark here"), self.insertMark))
75                 else:
76                         menu.append((_("remove this mark"), self.removeMark))
77
78                 FixedMenu.__init__(self, session, _("Cut"), menu)
79                 self.skinName = "Menu"
80
81         def startCut(self):
82                 self.close(self.RET_STARTCUT)
83
84         def endCut(self):
85                 self.close(self.RET_ENDCUT)
86
87         def deleteCut(self):
88                 self.close(self.RET_DELETECUT)
89
90         def insertMark(self):
91                 self.close(self.RET_MARK)
92
93         def removeMark(self):
94                 self.close(self.RET_DELETEMARK)
95
96         def removeBefore(self):
97                 self.close(self.RET_REMOVEBEFORE)
98
99         def removeAfter(self):
100                 self.close(self.RET_REMOVEAFTER)
101
102
103 class CutList(GUIComponent):
104         def __init__(self, list):
105                 GUIComponent.__init__(self)
106                 self.l = eListboxPythonMultiContent()
107                 self.setList(list)
108                 self.l.setFont(0, gFont("Regular", 20))
109                 self.onSelectionChanged = [ ]
110
111         def getCurrent(self):
112                 return self.l.getCurrentSelection()
113
114         def getCurrentIndex(self):
115                 return self.l.getCurrentSelectionIndex()
116
117         GUI_WIDGET = eListbox
118
119         def postWidgetCreate(self, instance):
120                 instance.setContent(self.l)
121                 instance.setItemHeight(30)
122                 instance.selectionChanged.get().append(self.selectionChanged)
123
124         def preWidgetRemove(self, instance):
125                 instance.setContent(None)
126                 instance.selectionChanged.get().remove(self.selectionChanged)
127
128         def selectionChanged(self):
129                 for x in self.onSelectionChanged:
130                         x()
131
132         def invalidateEntry(self, index):
133                 self.l.invalidateEntry(index)
134
135         def setIndex(self, index, data):
136                 self.list[index] = data
137                 self.invalidateEntry(index)
138
139         def setList(self, list):
140                 self.list = list
141                 self.l.setList(self.list)
142
143         def setSelection(self, index):
144                 if self.instance is not None:
145                         self.instance.moveSelectionTo(index)
146
147 class CutListEditor(Screen, InfoBarBase, InfoBarSeek, InfoBarCueSheetSupport, HelpableScreen):
148         skin = """
149         <screen position="0,0" size="720,576" title="Cutlist editor" flags="wfNoBorder">
150                 <eLabel text="Cutlist editor" position="65,60" size="300,25" font="Regular;20" />
151                 <widget source="global.CurrentTime" render="Label" position="268,60" size="394,20" font="Regular;20" halign="right">
152                         <convert type="ClockToText">Format:%A %B %d, %H:%M</convert>
153                 </widget>
154                 <eLabel position="268,98" size="394,304" backgroundColor="#505555" />
155                 <widget name="Video" position="270,100" zPosition="1" size="390,300" backgroundColor="transparent" />
156                 <widget source="session.CurrentService" render="Label" position="135,405" size="450,50" font="Regular;22" halign="center" valign="center">
157                         <convert type="ServiceName">Name</convert>
158                 </widget>
159                 <widget source="session.CurrentService" render="Label" position="50,450" zPosition="1" size="620,25" font="Regular;20" halign="center" valign="center">
160                         <convert type="ServicePosition">Position,Detailed</convert>
161                 </widget>
162                 <eLabel position="62,98" size="179,274" backgroundColor="#505555" />
163                 <eLabel position="64,100" size="175,270" backgroundColor="#000000" />
164                 <widget name="Cutlist" position="64,100" zPosition="1" size="175,270" scrollbarMode="showOnDemand" transparent="1" />
165                 <widget name="Timeline" position="50,485" size="615,20" backgroundColor="#505555" pointer="skin_default/position_arrow.png:3,5" foregroundColor="black" />
166                 <ePixmap pixmap="skin_default/icons/mp_buttons.png" position="305,515" size="109,13" alphatest="on" />
167         </screen>"""
168
169         def __init__(self, session, service):
170                 self.skin = CutListEditor.skin
171                 Screen.__init__(self, session)
172                 InfoBarSeek.__init__(self, actionmap = "CutlistSeekActions")
173                 InfoBarCueSheetSupport.__init__(self)
174                 InfoBarBase.__init__(self, steal_current_service = True)
175                 HelpableScreen.__init__(self)
176                 self.old_service = session.nav.getCurrentlyPlayingServiceReference()
177                 session.nav.playService(service)
178
179                 service = session.nav.getCurrentService()
180                 cue = service and service.cueSheet()
181                 if cue is not None:
182                         # disable cutlists. we want to freely browse around in the movie
183                         print "cut lists disabled!"
184                         cue.setCutListEnable(0)
185
186                 self.downloadCuesheet()
187
188                 self["Timeline"] = ServicePositionGauge(self.session.nav)
189                 self["Cutlist"] = CutList(self.getCutlist())
190                 self["Cutlist"].onSelectionChanged.append(self.selectionChanged)
191
192                 self["Video"] = VideoWindow(decoder = 0)
193
194                 self["actions"] = HelpableActionMap(self, "CutListEditorActions",
195                         {
196                                 "setIn": (self.setIn, _("Make this mark an 'in' point")),
197                                 "setOut": (self.setOut, _("Make this mark an 'out' point")),
198                                 "setMark": (self.setMark, _("Make this mark just a mark")),
199                                 "addMark": (self.__addMark, _("Add a mark")),
200                                 "removeMark": (self.__removeMark, _("Remove a mark")),
201                                 "leave": (self.exit, _("Exit editor")),
202                                 "showMenu": (self.showMenu, _("menu")),
203                         }, prio=-4)
204
205                 self.tutorial_seen = False
206
207                 self.onExecBegin.append(self.showTutorial)
208                 self.__event_tracker = ServiceEventTracker(screen=self, eventmap=
209                         {
210                                 iPlayableService.evCuesheetChanged: self.refillList
211                         })
212
213                 # to track new entries we save the last version of the cutlist
214                 self.last_cuts = [ ]
215                 self.cut_start = None
216                 self.onClose.append(self.__onClose)
217
218         def __onClose(self):
219                 self.session.nav.playService(self.old_service)
220
221         def showTutorial(self):
222                 if not self.tutorial_seen:
223                         self.tutorial_seen = True
224                         self.session.open(MessageBox, 
225                                 """Welcome to the Cutlist editor. 
226
227 Seek to the start of the stuff you want to cut away. Press OK, select 'start cut'.
228
229 Then seek to the end, press OK, select 'end cut'. That's it.
230                                 """, MessageBox.TYPE_INFO)
231
232         def checkSkipShowHideLock(self):
233                 pass
234
235         def setType(self, index, type):
236                 if len(self.cut_list):
237                         self.cut_list[index] = (self.cut_list[index][0], type)
238                         self["Cutlist"].setIndex(index, CutListEntry(*self.cut_list[index]))
239
240         def setIn(self):
241                 m = self["Cutlist"].getCurrentIndex()
242                 self.setType(m, 0)
243                 self.uploadCuesheet()
244
245         def setOut(self):
246                 m = self["Cutlist"].getCurrentIndex()
247                 self.setType(m, 1)
248                 self.uploadCuesheet()
249
250         def setMark(self):
251                 m = self["Cutlist"].getCurrentIndex()
252                 self.setType(m, 2)
253                 self.uploadCuesheet()
254
255         def __addMark(self):
256                 self.toggleMark(onlyadd=True, tolerance=90000) # do not allow two marks in <1s
257
258         def __removeMark(self):
259                 m = self["Cutlist"].getCurrent()
260                 m = m and m[0]
261                 if m is not None:
262                         self.removeMark(m)
263
264         def exit(self):
265                 self.close()
266
267         def getCutlist(self):
268                 r = [ ]
269                 for e in self.cut_list:
270                         r.append(CutListEntry(*e))
271                 return r
272
273         def selectionChanged(self):
274                 where = self["Cutlist"].getCurrent()
275                 if where is None:
276                         print "no selection"
277                         return
278                 pts = where[0][0]
279                 seek = self.getSeek()
280                 if seek is None:
281                         print "no seek"
282                         return
283                 seek.seekTo(pts)
284
285         def refillList(self):
286                 print "cue sheet changed, refilling"
287                 self.downloadCuesheet()
288
289                 # get the first changed entry, and select it
290                 new_list = self.getCutlist()
291                 self["Cutlist"].setList(new_list)
292
293                 for i in range(min(len(new_list), len(self.last_cuts))):
294                         if new_list[i] != self.last_cuts[i]:
295                                 self["Cutlist"].setSelection(i)
296                                 break
297                 self.last_cuts = new_list
298
299         def getStateForPosition(self, pos):
300                 state = 0 # in
301
302                 # when first point is "in", the beginning is "out"
303                 if len(self.cut_list) and self.cut_list[0][1] == 0:
304                         state = 1
305
306                 for (where, what) in self.cut_list:
307                         if where < pos:
308                                 if what == 0: # in
309                                         state = 0
310                                 elif what == 1: # out
311                                         state = 1
312                 return state
313
314         def showMenu(self):
315                 curpos = self.cueGetCurrentPosition()
316                 if curpos is None:
317                         return
318
319                 self.setSeekState(self.SEEK_STATE_PAUSE)
320
321                 self.context_position = curpos
322
323                 self.context_nearest_mark = self.toggleMark(onlyreturn=True)
324
325                 cur_state = self.getStateForPosition(curpos)
326                 if cur_state == 0:
327                         print "currently in 'IN'"
328                         if self.cut_start is None or self.context_position < self.cut_start:
329                                 state = CutListContextMenu.SHOW_STARTCUT
330                         else:
331                                 state = CutListContextMenu.SHOW_ENDCUT
332                 else:
333                         print "currently in 'OUT'"
334                         state = CutListContextMenu.SHOW_DELETECUT
335
336                 if self.context_nearest_mark is None:
337                         nearmark = False
338                 else:
339                         nearmark = True
340
341                 self.session.openWithCallback(self.menuCallback, CutListContextMenu, state, nearmark)
342
343         def menuCallback(self, *result):
344                 if not len(result):
345                         return
346                 result = result[0]
347
348                 if result == CutListContextMenu.RET_STARTCUT:
349                         self.cut_start = self.context_position
350                 elif result == CutListContextMenu.RET_ENDCUT:
351                         # remove in/out marks between the new cut
352                         for (where, what) in self.cut_list[:]:
353                                 if self.cut_start <= where <= self.context_position and what in [0,1]:
354                                         self.cut_list.remove((where, what))
355
356                         bisect.insort(self.cut_list, (self.cut_start, 1))
357                         bisect.insort(self.cut_list, (self.context_position, 0))
358                         self.uploadCuesheet()
359                         self.cut_start = None
360                 elif result == CutListContextMenu.RET_DELETECUT:
361                         out_before = None
362                         in_after = None
363
364                         for (where, what) in self.cut_list:
365                                 if what == 1 and where < self.context_position: # out
366                                         out_before = (where, what)
367                                 elif what == 0 and where < self.context_position: # in, before out
368                                         out_before = None
369                                 elif what == 0 and where > self.context_position and in_after is None:
370                                         in_after = (where, what)
371
372                         if out_before is not None:
373                                 self.cut_list.remove(out_before)
374
375                         if in_after is not None:
376                                 self.cut_list.remove(in_after)
377                         self.uploadCuesheet()
378                 elif result == CutListContextMenu.RET_MARK:
379                         self.__addMark()
380                 elif result == CutListContextMenu.RET_DELETEMARK:
381                         self.cut_list.remove(self.context_nearest_mark)
382                         self.uploadCuesheet()
383                 elif result == CutListContextMenu.RET_REMOVEBEFORE:
384                         # remove in/out marks before current position
385                         for (where, what) in self.cut_list[:]:
386                                 if where <= self.context_position and what in [0,1]:
387                                         self.cut_list.remove((where, what))
388                         # add 'in' point
389                         bisect.insort(self.cut_list, (self.context_position, 0))
390                         self.uploadCuesheet()
391                 elif result == CutListContextMenu.RET_REMOVEAFTER:
392                         # remove in/out marks after current position
393                         for (where, what) in self.cut_list[:]:
394                                 if where >= self.context_position and what in [0,1]:
395                                         self.cut_list.remove((where, what))
396                         # add 'out' point
397                         bisect.insort(self.cut_list, (self.context_position, 1))
398                         self.uploadCuesheet()
399
400         # we modify the "play" behavior a bit:
401         # if we press pause while being in slowmotion, we will pause (and not play)
402         def playpauseService(self):
403                 if self.seekstate != self.SEEK_STATE_PLAY and not self.isStateSlowMotion(self.seekstate):
404                         self.unPauseService()
405                 else:
406                         self.pauseService()
407
408 def main(session, service, **kwargs):
409         session.open(CutListEditor, service)
410
411 def Plugins(**kwargs):
412         return PluginDescriptor(name="Cutlist Editor", description=_("Cutlist editor..."), where = PluginDescriptor.WHERE_MOVIELIST, fnc=main)