tweak cutlist editor UI a bit
[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.MenuList import MenuList
8 from Components.MultiContent import MultiContentEntryText
9 from Components.ServiceEventTracker import ServiceEventTracker
10 from Components.VideoWindow import VideoWindow
11 from Screens.InfoBarGenerics import InfoBarSeek, InfoBarCueSheetSupport, InfoBarServiceName
12 from Components.GUIComponent import GUIComponent
13 from enigma import eListboxPythonMultiContent, eListbox, gFont, iPlayableService, RT_HALIGN_RIGHT
14 from Screens.FixedMenu import FixedMenu
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 selectionChanged(self):
125                 for x in self.onSelectionChanged:
126                         x()
127
128         def invalidateEntry(self, index):
129                 self.l.invalidateEntry(index)
130
131         def setIndex(self, index, data):
132                 self.list[index] = data
133                 self.invalidateEntry(index)
134
135         def setList(self, list):
136                 self.list = list
137                 self.l.setList(self.list)
138
139         def setSelection(self, index):
140                 if self.instance is not None:
141                         self.instance.moveSelectionTo(index)
142
143 class CutListEditor(Screen, InfoBarSeek, InfoBarCueSheetSupport, InfoBarServiceName):
144         skin = """
145                 <screen position="0,0" size="720,576" flags="wfNoBorder" backgroundColor="#444444">
146                         <eLabel position="360,0" size="360,313" backgroundColor="#ffffff" />
147                         <widget name="Video" position="370,10" size="340,268" backgroundColor="transparent" zPosition="1" />
148
149                         <eLabel position="50,80" size="300,24" text="Name:" font="Regular;20" foregroundColor="#cccccc" transparent="1" />
150
151                         <widget source="CurrentService" render="Label" position="50,110" size="300,60" font="Regular;22" >
152                                 <convert type="ServiceName">Name</convert>
153                         </widget>
154
155                         <widget source="CurrentService" render="Label" position="370,278" size="340,25" 
156                                 backgroundColor="#000000" foregroundColor="#ffffff" font="Regular;19" zPosition="1" >
157                                 <convert type="ServicePosition">PositionDetailed</convert>
158                         </widget>
159
160                         <widget name="Timeline" position="50,500" size="620,40" transparent="1"
161                                 pointer="/usr/share/enigma2/position_pointer.png:3,5" foregroundColor="#225b7395" />
162                         <widget name="Cutlist" position="50,325" size="620,175" scrollbarMode="showOnDemand" transparent="1" />
163                 </screen>"""
164         def __init__(self, session, service):
165                 self.skin = CutListEditor.skin
166                 Screen.__init__(self, session)
167                 InfoBarSeek.__init__(self, actionmap = "CutlistSeekActions")
168                 InfoBarCueSheetSupport.__init__(self)
169                 InfoBarServiceName.__init__(self)
170                 self.old_service = session.nav.getCurrentlyPlayingServiceReference()
171                 session.nav.playService(service)
172
173                 service = session.nav.getCurrentService()
174                 cue = service and service.cueSheet()
175                 if cue is not None:
176                         # disable cutlists. we want to freely browse around in the movie
177                         print "cut lists disabled!"
178                         cue.setCutListEnable(0)
179
180                 self.downloadCuesheet()
181
182                 self["Timeline"] = ServicePositionGauge(self.session.nav)
183                 self["Cutlist"] = CutList(self.getCutlist())
184                 self["Cutlist"].onSelectionChanged.append(self.selectionChanged)
185
186                 self["Video"] = VideoWindow(decoder = 0)
187
188                 self["actions"] = HelpableActionMap(self, "CutListEditorActions",
189                         {
190                                 "setIn": (self.setIn, _("Make this mark an 'in' point")),
191                                 "setOut": (self.setOut, _("Make this mark an 'out' point")),
192                                 "setMark": (self.setMark, _("Make this mark just a mark")),
193                                 "addMark": (self.__addMark, _("Add a mark")),
194                                 "removeMark": (self.__removeMark, _("Remove a mark")),
195                                 "leave": (self.exit, _("Exit editor")),
196                                 "showMenu": self.showMenu,
197                         }, prio=-4)
198
199                 self.tutorial_seen = False
200
201                 self.onExecBegin.append(self.showTutorial)
202                 self.__event_tracker = ServiceEventTracker(screen=self, eventmap=
203                         {
204                                 iPlayableService.evCuesheetChanged: self.refillList
205                         })
206
207                 # to track new entries we save the last version of the cutlist
208                 self.last_cuts = [ ]
209                 self.cut_start = None
210
211         def showTutorial(self):
212                 if not self.tutorial_seen:
213                         self.tutorial_seen = True
214                         self.session.open(MessageBox, 
215                                 """Welcome to the Cutlist editor. It's still a bit strange to use, but anyway:
216
217 Seek to the start of the stuff you want to cut away. Press OK, select 'start cut'.
218
219 Then seek to the end, press OK, select 'end cut'. That's it.
220                                 """, MessageBox.TYPE_INFO)
221
222         def checkSkipShowHideLock(self):
223                 pass
224
225         def setType(self, index, type):
226                 if len(self.cut_list):
227                         self.cut_list[index] = (self.cut_list[index][0], type)
228                         self["Cutlist"].setIndex(index, CutListEntry(*self.cut_list[index]))
229
230         def setIn(self):
231                 m = self["Cutlist"].getCurrentIndex()
232                 self.setType(m, 0)
233                 self.uploadCuesheet()
234
235         def setOut(self):
236                 m = self["Cutlist"].getCurrentIndex()
237                 self.setType(m, 1)
238                 self.uploadCuesheet()
239
240         def setMark(self):
241                 m = self["Cutlist"].getCurrentIndex()
242                 self.setType(m, 2)
243                 self.uploadCuesheet()
244
245         def __addMark(self):
246                 self.toggleMark(onlyadd=True, tolerance=90000) # do not allow two marks in <1s
247
248         def __removeMark(self):
249                 m = self["Cutlist"].getCurrent()
250                 m = m and m[0]
251                 if m is not None:
252                         self.removeMark(m)
253
254         def exit(self):
255                 self.session.nav.playService(self.old_service)
256                 self.close()
257
258         def getCutlist(self):
259                 r = [ ]
260                 for e in self.cut_list:
261                         r.append(CutListEntry(*e))
262                 return r
263
264         def selectionChanged(self):
265                 where = self["Cutlist"].getCurrent()
266                 if where is None:
267                         print "no selection"
268                         return
269                 pts = where[0][0]
270                 seek = self.getSeek()
271                 if seek is None:
272                         print "no seek"
273                         return
274                 seek.seekTo(pts)
275
276         def refillList(self):
277                 print "cue sheet changed, refilling"
278                 self.downloadCuesheet()
279
280                 # get the first changed entry, and select it
281                 new_list = self.getCutlist()
282                 self["Cutlist"].setList(new_list)
283
284                 for i in range(min(len(new_list), len(self.last_cuts))):
285                         if new_list[i] != self.last_cuts[i]:
286                                 self["Cutlist"].setSelection(i)
287                                 break
288                 self.last_cuts = new_list
289
290         def getStateForPosition(self, pos):
291                 state = 0 # in
292
293                 # when first point is "in", the beginning is "out"
294                 if len(self.cut_list) and self.cut_list[0][1] == 0:
295                         state = 1
296
297                 for (where, what) in self.cut_list:
298                         if where < pos:
299                                 if what == 0: # in
300                                         state = 0
301                                 elif what == 1: # out
302                                         state = 1
303                 return state
304
305         def showMenu(self):
306                 curpos = self.cueGetCurrentPosition()
307                 if curpos is None:
308                         return
309
310                 self.setSeekState(self.SEEK_STATE_PAUSE)
311
312                 self.context_position = curpos
313
314                 self.context_nearest_mark = self.toggleMark(onlyreturn=True)
315
316                 cur_state = self.getStateForPosition(curpos)
317                 if cur_state == 0:
318                         print "currently in 'IN'"
319                         if self.cut_start is None or self.context_position < self.cut_start:
320                                 state = CutListContextMenu.SHOW_STARTCUT
321                         else:
322                                 state = CutListContextMenu.SHOW_ENDCUT
323                 else:
324                         print "currently in 'OUT'"
325                         state = CutListContextMenu.SHOW_DELETECUT
326
327                 if self.context_nearest_mark is None:
328                         nearmark = False
329                 else:
330                         nearmark = True
331
332                 self.session.openWithCallback(self.menuCallback, CutListContextMenu, state, nearmark)
333
334         def menuCallback(self, *result):
335                 if not len(result):
336                         return
337                 result = result[0]
338
339                 if result == CutListContextMenu.RET_STARTCUT:
340                         self.cut_start = self.context_position
341                 elif result == CutListContextMenu.RET_ENDCUT:
342                         # remove in/out marks between the new cut
343                         for (where, what) in self.cut_list[:]:
344                                 if self.cut_start <= where <= self.context_position and what in [0,1]:
345                                         self.cut_list.remove((where, what))
346
347                         bisect.insort(self.cut_list, (self.cut_start, 1))
348                         bisect.insort(self.cut_list, (self.context_position, 0))
349                         self.uploadCuesheet()
350                         self.cut_start = None
351                 elif result == CutListContextMenu.RET_DELETECUT:
352                         out_before = None
353                         in_after = None
354
355                         for (where, what) in self.cut_list:
356                                 if what == 1 and where < self.context_position: # out
357                                         out_before = (where, what)
358                                 elif what == 0 and where < self.context_position: # in, before out
359                                         out_before = None
360                                 elif what == 0 and where > self.context_position and in_after is None:
361                                         in_after = (where, what)
362
363                         if out_before is not None:
364                                 self.cut_list.remove(out_before)
365
366                         if in_after is not None:
367                                 self.cut_list.remove(in_after)
368                         self.uploadCuesheet()
369                 elif result == CutListContextMenu.RET_MARK:
370                         self.__addMark()
371                 elif result == CutListContextMenu.RET_DELETEMARK:
372                         self.cut_list.remove(self.context_nearest_mark)
373                         self.uploadCuesheet()
374                 elif result == CutListContextMenu.RET_REMOVEBEFORE:
375                         # remove in/out marks before current position
376                         for (where, what) in self.cut_list[:]:
377                                 if where <= self.context_position and what in [0,1]:
378                                         self.cut_list.remove((where, what))
379                         # add 'in' point
380                         bisect.insort(self.cut_list, (self.context_position, 0))
381                         self.uploadCuesheet()
382                 elif result == CutListContextMenu.RET_REMOVEAFTER:
383                         # remove in/out marks after current position
384                         for (where, what) in self.cut_list[:]:
385                                 if where >= self.context_position and what in [0,1]:
386                                         self.cut_list.remove((where, what))
387                         # add 'out' point
388                         bisect.insort(self.cut_list, (self.context_position, 1))
389                         self.uploadCuesheet()
390
391         # we modify the "play" behaviour a bit:
392         # if we press pause while being in slowmotion, we will pause (and not play)
393         def playpauseService(self):
394                 if self.seekstate not in [self.SEEK_STATE_PLAY, self.SEEK_STATE_SM_HALF, self.SEEK_STATE_SM_QUARTER, self.SEEK_STATE_SM_EIGHTH]:
395                         self.unPauseService()
396                 else:
397                         self.pauseService()
398
399 def main(session, service, **kwargs):
400         session.open(CutListEditor, service)
401
402 def Plugins(**kwargs):
403         return PluginDescriptor(name="Cutlist Editor", description=_("Cutlist editor..."), where = PluginDescriptor.WHERE_MOVIELIST, fnc=main)