~nico-inattendu/luciole/bug_740324

« back to all changes in this revision

Viewing changes to pitivi/ui/viewer.py

  • Committer: NicoInattendu
  • Date: 2011-02-28 18:27:56 UTC
  • mfrom: (123.1.54 luciole-with-sound)
  • Revision ID: nico@inattendu.org-20110228182756-weonszu8zpzermrl
initial merge with luciole-with-sound branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# PiTiVi , Non-linear video editor
 
3
#
 
4
#       ui/viewer.py
 
5
#
 
6
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
 
7
#
 
8
# This program is free software; you can redistribute it and/or
 
9
# modify it under the terms of the GNU Lesser General Public
 
10
# License as published by the Free Software Foundation; either
 
11
# version 2.1 of the License, or (at your option) any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
16
# Lesser General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU Lesser General Public
 
19
# License along with this program; if not, write to the
 
20
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 
21
# Boston, MA 02111-1307, USA.
 
22
 
 
23
import platform
 
24
import gobject
 
25
import gtk
 
26
from gtk import gdk
 
27
import gst
 
28
 
 
29
from gettext import gettext as _
 
30
 
 
31
from pitivi.action import ViewAction
 
32
 
 
33
from pitivi.stream import VideoStream
 
34
from pitivi.utils import time_to_string, Seeker
 
35
from pitivi.log.loggable import Loggable
 
36
from pitivi.pipeline import PipelineError
 
37
from pitivi.ui.common import SPACING
 
38
 
 
39
class ViewerError(Exception):
 
40
    pass
 
41
 
 
42
# TODO : Switch to using Pipeline and Action
 
43
 
 
44
class PitiviViewer(gtk.VBox, Loggable):
 
45
 
 
46
    __gtype_name__ = 'PitiviViewer'
 
47
    __gsignals__ = {
 
48
        "activate-playback-controls" : (gobject.SIGNAL_RUN_LAST, 
 
49
            gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)),
 
50
    }
 
51
 
 
52
    """
 
53
    A Widget to control and visualize a Pipeline
 
54
 
 
55
    @cvar pipeline: The current pipeline
 
56
    @type pipeline: L{Pipeline}
 
57
    @cvar action: The action controlled by this Pipeline
 
58
    @type action: L{ViewAction}
 
59
    """
 
60
 
 
61
    def __init__(self, action=None, pipeline=None):
 
62
        """
 
63
        @param action: Specific action to use instead of auto-created one
 
64
        @type action: L{ViewAction}
 
65
        """
 
66
        gtk.VBox.__init__(self)
 
67
        self.set_border_width(SPACING)
 
68
 
 
69
        Loggable.__init__(self)
 
70
        self.log("New PitiviViewer")
 
71
 
 
72
        self.seeker = Seeker(80)
 
73
        self.seeker.connect('seek', self._seekerSeekCb)
 
74
        self.action = action
 
75
        self.pipeline = pipeline
 
76
 
 
77
        self.current_time = long(0)
 
78
        self._initial_seek = None
 
79
        self.current_frame = -1
 
80
 
 
81
        self.currentState = gst.STATE_PAUSED
 
82
        self._haveUI = False
 
83
 
 
84
        self._createUi()
 
85
        self.setAction(action)
 
86
        self.setPipeline(pipeline)
 
87
 
 
88
    def setPipeline(self, pipeline):
 
89
        """
 
90
        Set the Viewer to the given Pipeline.
 
91
 
 
92
        Properly switches the currently set action to that new Pipeline.
 
93
 
 
94
        @param pipeline: The Pipeline to switch to.
 
95
        @type pipeline: L{Pipeline}.
 
96
        """
 
97
        self.debug("self.pipeline:%r, pipeline:%r", self.pipeline, pipeline)
 
98
 
 
99
        if pipeline is not None and pipeline == self.pipeline:
 
100
            return
 
101
 
 
102
        if self.pipeline != None:
 
103
            # remove previously set Pipeline
 
104
            self._disconnectFromPipeline()
 
105
            # make ui inactive
 
106
            self._setUiActive(False)
 
107
            # finally remove previous pipeline
 
108
            self.pipeline = None
 
109
            self.currentState = gst.STATE_PAUSED
 
110
            self.playpause_button.setPause()
 
111
        self._connectToPipeline(pipeline)
 
112
        self.pipeline = pipeline
 
113
        if self.pipeline is not None:
 
114
            self._setUiActive()
 
115
 
 
116
    def setAction(self, action):
 
117
        """
 
118
        Set the controlled action.
 
119
 
 
120
        @param action: The Action to set. If C{None}, a default L{ViewAction}
 
121
        will be used.
 
122
        @type action: L{ViewAction} or C{None}
 
123
        """
 
124
        self.debug("self.action:%r, action:%r", self.action, action)
 
125
        if action is not None and action == self.action:
 
126
            return
 
127
 
 
128
        if self.action != None:
 
129
            # if there was one previously, remove it
 
130
            self._disconnectFromAction()
 
131
        if action == None:
 
132
            # get the default action
 
133
            action = self._getDefaultAction()
 
134
        self._connectToAction(action)
 
135
        self.showControls()
 
136
 
 
137
    def _connectToPipeline(self, pipeline):
 
138
        self.debug("pipeline:%r", pipeline)
 
139
        if self.pipeline != None:
 
140
            raise ViewerError("previous pipeline wasn't disconnected")
 
141
        self.pipeline = pipeline
 
142
        if self.pipeline == None:
 
143
            return
 
144
        self.pipeline.connect('position', self._posCb)
 
145
        self.pipeline.activatePositionListener()
 
146
        self.pipeline.connect('state-changed', self._currentStateCb)
 
147
        self.pipeline.connect('element-message', self._elementMessageCb)
 
148
        self.pipeline.connect('duration-changed', self._durationChangedCb)
 
149
        self.pipeline.connect('eos', self._eosCb)
 
150
        # if we have an action set it to that new pipeline
 
151
        if self.action:
 
152
            self.pipeline.setAction(self.action)
 
153
            self.action.activate()
 
154
 
 
155
    def _disconnectFromPipeline(self):
 
156
        self.debug("pipeline:%r", self.pipeline)
 
157
        if self.pipeline == None:
 
158
            # silently return, there's nothing to disconnect from
 
159
            return
 
160
        if self.action and (self.action in self.pipeline.actions):
 
161
            # if we have an action, properly remove it from pipeline
 
162
            if self.action.isActive():
 
163
                self.pipeline.stop()
 
164
                self.action.deactivate()
 
165
            self.pipeline.removeAction(self.action)
 
166
 
 
167
        self.pipeline.disconnect_by_function(self._posCb)
 
168
        self.pipeline.disconnect_by_function(self._currentStateCb)
 
169
        self.pipeline.disconnect_by_function(self._elementMessageCb)
 
170
        self.pipeline.disconnect_by_function(self._durationChangedCb)
 
171
        self.pipeline.disconnect_by_function(self._eosCb)
 
172
        self.pipeline.stop()
 
173
 
 
174
        self.pipeline = None
 
175
 
 
176
    def _connectToAction(self, action):
 
177
        self.debug("action: %r", action)
 
178
        # not sure what we need to do ...
 
179
        self.action = action
 
180
        # FIXME: fix this properly?
 
181
        self.drawingarea.action = action
 
182
        dar = float(4/3)
 
183
        try:
 
184
            producer = action.producers[0]
 
185
            self.debug("producer:%r", producer)
 
186
            for stream in producer.output_streams:
 
187
                self.warning("stream:%r", stream)
 
188
            for stream in producer.getOutputStreams(VideoStream):
 
189
                self.debug("stream:%r", stream)
 
190
                if stream.dar:
 
191
                    dar = stream.dar
 
192
                    continue
 
193
        except:
 
194
            dar = float(4/3)
 
195
        self.setDisplayAspectRatio(dar)
 
196
        self.showControls()
 
197
 
 
198
    def _disconnectFromAction(self):
 
199
        self.action = None
 
200
 
 
201
    def _setUiActive(self, active=True):
 
202
        self.debug("active %r", active)
 
203
        self.set_sensitive(active)
 
204
        if self._haveUI:
 
205
            for item in [self.slider, self.goToStart_button, self.back_button,
 
206
                         self.playpause_button, self.forward_button,
 
207
                         self.goToEnd_button, self.timelabel]:
 
208
                item.set_sensitive(active)
 
209
        if active:
 
210
            self.emit("activate-playback-controls", True)
 
211
 
 
212
    def _getDefaultAction(self):
 
213
        return ViewAction()
 
214
 
 
215
    def _createUi(self):
 
216
        """ Creates the Viewer GUI """
 
217
        # drawing area
 
218
        self.aframe = gtk.AspectFrame(xalign=0.5, yalign=0.5, ratio=4.0/3.0,
 
219
                                      obey_child=False)
 
220
        self.pack_start(self.aframe, expand=True)
 
221
        self.drawingarea = ViewerWidget(self.action)
 
222
        self.aframe.add(self.drawingarea)
 
223
 
 
224
        # Slider
 
225
        self.posadjust = gtk.Adjustment()
 
226
        self.slider = gtk.HScale(self.posadjust)
 
227
        self.slider.set_draw_value(False)
 
228
        self.slider.connect("button-press-event", self._sliderButtonPressCb)
 
229
        self.slider.connect("button-release-event", self._sliderButtonReleaseCb)
 
230
        self.slider.connect("scroll-event", self._sliderScrollCb)
 
231
        self.pack_start(self.slider, expand=False)
 
232
        self.moving_slider = False
 
233
        self.slider.set_sensitive(False)
 
234
 
 
235
        # Buttons/Controls
 
236
        bbox = gtk.HBox()
 
237
        boxalign = gtk.Alignment(xalign=0.5, yalign=0.5)
 
238
        boxalign.add(bbox)
 
239
        self.pack_start(boxalign, expand=False)
 
240
 
 
241
        self.goToStart_button = gtk.ToolButton(gtk.STOCK_MEDIA_PREVIOUS)
 
242
        self.goToStart_button.connect("clicked", self._goToStartCb)
 
243
        self.goToStart_button.set_tooltip_text(_("Go to the beginning of the timeline"))
 
244
        self.goToStart_button.set_sensitive(False)
 
245
        bbox.pack_start(self.goToStart_button, expand=False)
 
246
 
 
247
        self.back_button = gtk.ToolButton(gtk.STOCK_MEDIA_REWIND)
 
248
        self.back_button.connect("clicked", self._backCb)
 
249
        self.back_button.set_tooltip_text(_("Go back one second"))
 
250
        self.back_button.set_sensitive(False)
 
251
        bbox.pack_start(self.back_button, expand=False)
 
252
 
 
253
        self.playpause_button = PlayPauseButton()
 
254
        self.playpause_button.connect("play", self._playButtonCb)
 
255
        bbox.pack_start(self.playpause_button, expand=False)
 
256
        self.playpause_button.set_sensitive(False)
 
257
 
 
258
        self.forward_button = gtk.ToolButton(gtk.STOCK_MEDIA_FORWARD)
 
259
        self.forward_button.connect("clicked", self._forwardCb)
 
260
        self.forward_button.set_tooltip_text(_("Go forward one second"))
 
261
        self.forward_button.set_sensitive(False)
 
262
        bbox.pack_start(self.forward_button, expand=False)
 
263
 
 
264
        self.goToEnd_button = gtk.ToolButton(gtk.STOCK_MEDIA_NEXT)
 
265
        self.goToEnd_button.connect("clicked", self._goToEndCb)
 
266
        self.goToEnd_button.set_tooltip_text(_("Go to the end of the timeline"))
 
267
        self.goToEnd_button.set_sensitive(False)
 
268
        bbox.pack_start(self.goToEnd_button, expand=False)
 
269
 
 
270
        # current time
 
271
        self.timelabel = gtk.Label()
 
272
        self.timelabel.set_markup("<tt>00:00:00.000</tt>")
 
273
        self.timelabel.set_alignment(1.0, 0.5)
 
274
        bbox.pack_start(self.timelabel, expand=False, padding=10)
 
275
        self._haveUI = True
 
276
 
 
277
        screen = gdk.screen_get_default()
 
278
        height = screen.get_height()
 
279
        if height >= 800:
 
280
            # show the controls and force the aspect frame to have at least the same
 
281
            # width (+110, which is a magic number to minimize dead padding).
 
282
            bbox.show_all()
 
283
            width, height = bbox.size_request()
 
284
            width += 110
 
285
            height = int(width / self.aframe.props.ratio)
 
286
            self.aframe.set_size_request(width , height)
 
287
        self.show_all()
 
288
 
 
289
    _showingSlider = True
 
290
 
 
291
    def showSlider(self):
 
292
        self._showingSlider = True
 
293
        self.slider.show()
 
294
 
 
295
    def hideSlider(self):
 
296
        self._showingSlider = False
 
297
        self.slider.hide()
 
298
 
 
299
    def showControls(self):
 
300
        if not self.action:
 
301
            return
 
302
        if True:
 
303
            self.goToStart_button.show()
 
304
            self.back_button.show()
 
305
            self.playpause_button.show()
 
306
            self.forward_button.show()
 
307
            self.goToEnd_button.show()
 
308
            if self._showingSlider:
 
309
                self.slider.show()
 
310
        else:
 
311
            self.goToStart_button.hide()
 
312
            self.back_button.hide()
 
313
            self.playpause_button.hide()
 
314
            self.forward_button.hide()
 
315
            self.goToEnd_button.hide()
 
316
            self.slider.hide()
 
317
 
 
318
    def setDisplayAspectRatio(self, ratio):
 
319
        """
 
320
        Sets the DAR of the Viewer to the given ratio.
 
321
 
 
322
        @arg ratio: The aspect ratio to set on the viewer
 
323
        @type ratio: L{float}
 
324
        """
 
325
        self.debug("Setting ratio of %f [%r]", float(ratio), ratio)
 
326
        try:
 
327
            self.aframe.set_property("ratio", float(ratio))
 
328
        except:
 
329
            self.warning("could not set ratio !")
 
330
 
 
331
    ## gtk.HScale callbacks for self.slider
 
332
 
 
333
    def _sliderButtonPressCb(self, slider, event):
 
334
        # borrow totem hack for seek-on-click behavior
 
335
        event.button = 2
 
336
        self.info("button pressed")
 
337
        self.moving_slider = True
 
338
        self.valuechangedid = slider.connect("value-changed", self._sliderValueChangedCb)
 
339
        self.pipeline.pause()
 
340
        return False
 
341
 
 
342
    def _sliderButtonReleaseCb(self, slider, event):
 
343
        # borrow totem hack for seek-on-click behavior
 
344
        event.button = 2
 
345
        self.info("slider button release at %s", time_to_string(long(slider.get_value())))
 
346
        self.moving_slider = False
 
347
        if self.valuechangedid:
 
348
            slider.disconnect(self.valuechangedid)
 
349
            self.valuechangedid = 0
 
350
        # revert to previous state
 
351
        if self.currentState == gst.STATE_PAUSED:
 
352
            self.pipeline.pause()
 
353
        else:
 
354
            self.pipeline.play()
 
355
        return False
 
356
 
 
357
    def _sliderValueChangedCb(self, slider):
 
358
        """ seeks when the value of the slider has changed """
 
359
        value = long(slider.get_value())
 
360
        self.info(gst.TIME_ARGS(value))
 
361
        if self.moving_slider:
 
362
            self.seek(value)
 
363
 
 
364
    def _sliderScrollCb(self, unused_slider, event):
 
365
        if event.direction == gtk.gdk.SCROLL_LEFT:
 
366
            amount = -gst.SECOND
 
367
        else:
 
368
            amount = gst.SECOND
 
369
        self.seekRelative(amount)
 
370
 
 
371
    def seek(self, position, format=gst.FORMAT_TIME):
 
372
        try:
 
373
            self.seeker.seek(position, format)
 
374
        except:
 
375
            self.warning("seek failed")
 
376
 
 
377
    def _seekerSeekCb(self, seeker, position, format):
 
378
        try:
 
379
            self.pipeline.seek(position, format)
 
380
        except PipelineError:
 
381
            self.error("seek failed %s %s", gst.TIME_ARGS(position), format)
 
382
 
 
383
    def _newTime(self, value, frame=-1):
 
384
        self.info("value:%s, frame:%d", gst.TIME_ARGS(value), frame)
 
385
        self.current_time = value
 
386
        self.current_frame = frame
 
387
        self.timelabel.set_markup("<tt>%s</tt>" % time_to_string(value))
 
388
        if not self.moving_slider:
 
389
            self.posadjust.set_value(float(value))
 
390
        return False
 
391
 
 
392
 
 
393
    ## active Timeline calllbacks
 
394
 
 
395
    def _durationChangedCb(self, unused_pipeline, duration):
 
396
        self.debug("duration : %s", gst.TIME_ARGS(duration))
 
397
        position = self.posadjust.get_value()
 
398
        if duration < position:
 
399
            self.posadjust.set_value(float(duration))
 
400
        self.posadjust.upper = float(duration)
 
401
 
 
402
        if duration == 0:
 
403
            self._setUiActive(False)
 
404
        else:
 
405
            self._setUiActive(True)
 
406
 
 
407
        if self._initial_seek is not None:
 
408
            seek, self._initial_seek = self._initial_seek, None
 
409
            self.pipeline.seek(seek)
 
410
 
 
411
    ## Control gtk.Button callbacks
 
412
 
 
413
    def _goToStartCb(self, unused_button):
 
414
        self.seek(0)
 
415
 
 
416
    def _backCb(self, unused_button):
 
417
        self.seekRelative(-gst.SECOND)
 
418
 
 
419
    def _playButtonCb(self, unused_button, isplaying):
 
420
        self.togglePlayback()
 
421
 
 
422
    def _forwardCb(self, unused_button):
 
423
        self.seekRelative(gst.SECOND)
 
424
 
 
425
    def _goToEndCb(self, unused_button):
 
426
        try:
 
427
            dur = self.pipeline.getDuration()
 
428
            self.seek(dur - 1)
 
429
        except:
 
430
            self.warning("couldn't get duration")
 
431
 
 
432
    ## public methods for controlling playback
 
433
 
 
434
    def play(self):
 
435
        self.pipeline.play()
 
436
 
 
437
    def pause(self):
 
438
        self.pipeline.pause()
 
439
 
 
440
    def togglePlayback(self):
 
441
        if self.pipeline is None:
 
442
            return
 
443
        self.pipeline.togglePlayback()
 
444
 
 
445
    def seekRelative(self, time):
 
446
        try:
 
447
            self.pipeline.seekRelative(time)
 
448
        except:
 
449
            self.warning("seek failed")
 
450
 
 
451
    def _posCb(self, unused_pipeline, pos):
 
452
        self._newTime(pos)
 
453
 
 
454
    def _currentStateCb(self, unused_pipeline, state):
 
455
        self.info("current state changed : %s", state)
 
456
        if state == int(gst.STATE_PLAYING):
 
457
            self.playpause_button.setPause()
 
458
        elif state == int(gst.STATE_PAUSED):
 
459
            self.playpause_button.setPlay()
 
460
        self.currentState = state
 
461
 
 
462
    def _eosCb(self, unused_pipeline):
 
463
        self.playpause_button.setPlay()
 
464
 
 
465
    def _elementMessageCb(self, unused_pipeline, message):
 
466
        name = message.structure.get_name()
 
467
        self.log('message:%s / %s', message, name)
 
468
        if name == 'prepare-xwindow-id':
 
469
            sink = message.src
 
470
            sink.set_xwindow_id(self.drawingarea.window_xid)
 
471
 
 
472
 
 
473
class ViewerWidget(gtk.DrawingArea, Loggable):
 
474
    """
 
475
    Widget for displaying properly GStreamer video sink
 
476
    """
 
477
 
 
478
    __gsignals__ = {}
 
479
 
 
480
    def __init__(self, action):
 
481
        gtk.DrawingArea.__init__(self)
 
482
        Loggable.__init__(self)
 
483
        self.action = action # FIXME : Check if it's a view action
 
484
        self.unset_flags(gtk.SENSITIVE)
 
485
        for state in range(gtk.STATE_INSENSITIVE + 1):
 
486
            self.modify_bg(state, self.style.black)
 
487
 
 
488
    def do_realize(self):
 
489
        gtk.DrawingArea.do_realize(self)
 
490
        if platform.system() == 'Windows':
 
491
            self.window_xid = self.window.handle
 
492
        else:
 
493
            self.window_xid  = self.window.xid
 
494
 
 
495
class PlayPauseButton(gtk.Button, Loggable):
 
496
    """ Double state gtk.Button which displays play/pause """
 
497
 
 
498
    __gsignals__ = {
 
499
        "play" : ( gobject.SIGNAL_RUN_LAST,
 
500
                   gobject.TYPE_NONE,
 
501
                   (gobject.TYPE_BOOLEAN, ))
 
502
        }
 
503
 
 
504
    def __init__(self):
 
505
        gtk.Button.__init__(self, label="")
 
506
        Loggable.__init__(self)
 
507
        self.get_settings().props.gtk_button_images = True
 
508
        self.playing = True
 
509
        self.setPlay()
 
510
        self.connect('clicked', self._clickedCb)
 
511
 
 
512
    def set_sensitive(self, value):
 
513
        gtk.Button.set_sensitive(self, value)
 
514
 
 
515
    def _clickedCb(self, unused):
 
516
        self.emit("play", self.playing)
 
517
 
 
518
    def setPlay(self):
 
519
        """ display the play image """
 
520
        self.log("setPlay")
 
521
        if self.playing:
 
522
            self.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON))
 
523
            self.set_tooltip_text(_("Play"))
 
524
            self.playing = False
 
525
 
 
526
    def setPause(self):
 
527
        self.log("setPause")
 
528
        """ display the pause image """
 
529
        if not self.playing:
 
530
            self.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON))
 
531
            self.set_tooltip_text(_("Pause"))
 
532
            self.playing = True