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