Ejemplo n.º 1
0
class ViewerContainer(Gtk.VBox, Loggable):
    """
    A wiget holding a viewer and the controls.
    """
    __gtype_name__ = 'ViewerContainer'
    __gsignals__ = {
        "activate-playback-controls":
        (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_BOOLEAN, )),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app):
        Gtk.VBox.__init__(self)
        self.set_border_width(SPACING)
        self.app = app
        self.settings = app.settings
        self.system = app.system

        Loggable.__init__(self)
        self.log("New ViewerContainer")

        self.pipeline = None
        self.docked = True
        self.seeker = Seeker()

        # Only used for restoring the pipeline position after a live clip trim preview:
        self._oldTimelinePos = None

        self._haveUI = False

        self._createUi()

        if not self.settings.viewerDocked:
            self.undock()

    @property
    def target(self):
        if self.docked:
            return self.internal
        else:
            return self.external

    def setPipeline(self, pipeline, position=None):
        """
        Set the Viewer to the given Pipeline.

        Properly switches the currently set action to that new Pipeline.

        @param pipeline: The Pipeline to switch to.
        @type pipeline: L{Pipeline}.
        @param position: Optional position to seek to initially.
        """
        self._disconnectFromPipeline()

        self.debug("New pipeline: %r", pipeline)
        self.pipeline = pipeline
        self.pipeline.pause()
        self.seeker.seek(position)

        self.pipeline.connect("state-change", self._pipelineStateChangedCb)
        self.pipeline.connect("position", self._positionCb)
        self.pipeline.connect("duration-changed", self._durationChangedCb)

        self.sink = pipeline._opengl_sink

        self._switch_output_window()
        self._setUiActive()

    def _disconnectFromPipeline(self):
        self.debug("Previous pipeline: %r", self.pipeline)
        if self.pipeline is None:
            # silently return, there's nothing to disconnect from
            return

        self.pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        self.pipeline.disconnect_by_func(self._positionCb)
        self.pipeline.disconnect_by_func(self._durationChangedCb)

        self.pipeline = None

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        self.set_sensitive(active)
        if self._haveUI:
            for item in [
                    self.goToStart_button, self.back_button,
                    self.playpause_button, self.forward_button,
                    self.goToEnd_button, self.timecode_entry
            ]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, unused_window, unused_event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, unused_window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _videoRealizedCb(self, unused_drawing_area, viewer):
        if viewer == self.target:
            self.log("Viewer widget realized: %s", viewer)
            self._switch_output_window()

    def _createUi(self):
        """ Creates the Viewer GUI """
        # Drawing area
        self.internal = ViewerWidget(self.app.settings,
                                     realizedCb=self._videoRealizedCb)
        # Transformation boxed DISABLED
        # self.internal.init_transformation_events()
        self.pack_start(self.internal, True, True, 0)

        self.external_window = Gtk.Window()
        vbox = Gtk.VBox()
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external = ViewerWidget(self.app.settings,
                                     realizedCb=self._videoRealizedCb)
        vbox.pack_start(self.external, True, True, 0)
        self.external_window.connect("delete-event",
                                     self._externalWindowDeleteCb)
        self.external_window.connect("configure-event",
                                     self._externalWindowConfigureCb)
        self.external_vbox = vbox

        # Buttons/Controls
        bbox = Gtk.HBox()

        bbox.set_property("valign", Gtk.Align.CENTER)
        bbox.set_property("halign", Gtk.Align.CENTER)

        self.pack_start(bbox, False, True, SPACING)

        self.goToStart_button = Gtk.ToolButton()
        self.goToStart_button.set_icon_name("media-skip-backward")
        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_tooltip_text(
            _("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, True, 0)

        self.back_button = Gtk.ToolButton()
        self.back_button.set_icon_name("media-seek-backward")
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, True, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, True, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.ToolButton()
        self.forward_button.set_icon_name("media-seek-forward")
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, True, 0)

        self.goToEnd_button = Gtk.ToolButton()
        self.goToEnd_button.set_icon_name("media-skip-forward")
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(
            _("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, True, 0)

        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(
            _('Enter a timecode or frame number\nand press "Enter" to go to that position'
              ))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        bbox.pack_start(self.timecode_entry, False, 10, 0)

        self.undock_button = Gtk.ToolButton()
        self.undock_button.set_icon_name("view-restore")
        self.undock_button.connect("clicked", self.undock)
        self.undock_button.set_tooltip_text(
            _("Detach the viewer\nYou can re-attach it by closing the newly created window."
              ))
        bbox.pack_start(self.undock_button, False, True, 0)

        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")
        self.undock_button.get_accessible().set_name("undock_button")

        screen = Gdk.Screen.get_default()
        height = screen.get_height()
        if height >= 800:
            # show the controls and force the aspect frame to have at least the same
            # width (+110, which is a magic number to minimize dead padding).
            bbox.show_all()
            req = bbox.size_request()
            width = req.width
            height = req.height
            width += 110
            height = int(width / self.internal.props.ratio)
            self.internal.set_size_request(width, height)

        self.buttons = bbox
        self.buttons_container = bbox
        self.show_all()
        self.external_vbox.show_all()

    def setDisplayAspectRatio(self, ratio):
        self.debug("Setting aspect ratio to %f [%r]", float(ratio), ratio)
        self.internal.setDisplayAspectRatio(ratio)
        self.external.setDisplayAspectRatio(ratio)

    def _entryActivateCb(self, unused_entry):
        self._seekFromTimecodeWidget()

    def _seekFromTimecodeWidget(self):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.seeker.seek(nanoseconds)

    # Active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        if duration == 0:
            self._setUiActive(False)
        else:
            self._setUiActive(True)

    # Control Gtk.Button callbacks

    def setZoom(self, zoom):
        """
        Zoom in or out of the transformation box canvas.
        This is called by clipproperties.
        """
        if self.target.box:
            maxSize = self.target.area
            width = int(float(maxSize.width) * zoom)
            height = int(float(maxSize.height) * zoom)
            area = ((maxSize.width - width) / 2, (maxSize.height - height) / 2,
                    width, height)
            self.sink.set_render_rectangle(*area)
            self.target.box.update_size(area)
            self.target.zoom = zoom
            self.target.renderbox()

    def _playButtonCb(self, unused_button, unused_playing):
        self.app.project_manager.current_project.pipeline.togglePlayback()
        self.app.gui.focusTimeline()

    def _goToStartCb(self, unused_button):
        self.seeker.seek(0)
        self.app.gui.focusTimeline()

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.seeker.seekRelative(0 - Gst.SECOND)
        self.app.gui.focusTimeline()

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.seeker.seekRelative(Gst.SECOND)
        self.app.gui.focusTimeline()

    def _goToEndCb(self, unused_button):
        end = self.app.project_manager.current_project.pipeline.getDuration()
        self.seeker.seek(end)
        self.app.gui.focusTimeline()

    # Public methods for controlling playback

    def undock(self, *unused_widget):
        if not self.docked:
            self.warning("The viewer is already undocked")
            return

        self.docked = False
        self.settings.viewerDocked = False

        self.remove(self.buttons_container)
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)
        self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
        self.external_window.show()

        self.undock_button.hide()
        self.fullscreen_button = Gtk.ToggleToolButton()
        self.fullscreen_button.set_icon_name("view-fullscreen")
        self.fullscreen_button.set_tooltip_text(
            _("Show this window in fullscreen"))
        self.buttons.pack_end(self.fullscreen_button,
                              expand=False,
                              fill=False,
                              padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggleFullscreen)

        # if we are playing, switch output immediately
        if self.pipeline:
            self._switch_output_window()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(self.settings.viewerWidth,
                                    self.settings.viewerHeight)

    def dock(self):
        if self.docked:
            self.warning("The viewer is already docked")
            return
        self.docked = True
        self.settings.viewerDocked = True

        self.undock_button.show()
        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()
        # if we are playing, switch output immediately
        if self.pipeline:
            self._switch_output_window()
        self.external_window.hide()

    def _toggleFullscreen(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """
        If the timeline position changed, update the viewer UI widgets.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, tl_obj, position):
        """
        While a clip is being trimmed, show a live preview of it.
        """
        if isinstance(tl_obj,
                      GES.TitleClip) or tl_obj.props.is_image or not hasattr(
                          tl_obj, "get_uri"):
            self.log("%s is an image or has no URI, so not previewing trim" %
                     tl_obj)
            return False

        clip_uri = tl_obj.props.uri
        cur_time = time()
        if self.pipeline == self.app.project_manager.current_project.pipeline:
            self.debug("Creating temporary pipeline for clip %s, position %s",
                       clip_uri, format_ns(position))
            self._oldTimelinePos = self.pipeline.getPosition()
            self.setPipeline(AssetPipeline(tl_obj))
            self._lastClipTrimTime = cur_time

        if (cur_time - self._lastClipTrimTime
            ) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED:
            # Do not seek more than once every 200 ms (for performance)
            self.pipeline.simple_seek(position)
            self._lastClipTrimTime = cur_time

    def clipTrimPreviewFinished(self):
        """
        After trimming a clip, reset the project pipeline into the viewer.
        """
        if self.pipeline is not self.app.project_manager.current_project.pipeline:
            self.pipeline.setState(Gst.State.NULL)
            # Using pipeline.getPosition() here does not work because for some
            # reason it's a bit off, that's why we need self._oldTimelinePos.
            self.setPipeline(self.app.project_manager.current_project.pipeline,
                             self._oldTimelinePos)
            self.debug("Back to the project's pipeline")

    def _pipelineStateChangedCb(self, unused_pipeline, state):
        """
        When playback starts/stops, update the viewer widget,
        play/pause button and (un)inhibit the screensaver.

        This is meant to be called by mainwindow.
        """
        if int(state) == int(Gst.State.PLAYING):
            self.playpause_button.setPause()
            self.system.inhibitScreensaver(self.INHIBIT_REASON)
        elif int(state) == int(Gst.State.PAUSED):
            self.playpause_button.setPlay()
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        else:
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        self.internal._currentStateCb(self.pipeline, state)

    def _switch_output_window(self):
        # Don't do anything if we don't have a pipeline
        if self.pipeline is None:
            return

        if self.target.get_realized():
            self.debug("Connecting the pipeline to the viewer's texture")
            if platform.system() == 'Windows':
                xid = self.target.drawing_area.get_window().get_handle()
            else:
                xid = self.target.drawing_area.get_window().get_xid()

            self.sink.set_window_handle(xid)
            self.sink.expose()
        else:
            # Show the widget and wait for the realized callback
            self.log("Target is not realized, showing the widget")
            self.target.show()
Ejemplo n.º 2
0
class PitiviViewer(Gtk.VBox, Loggable):
    """
    A Widget to control and visualize a Pipeline

    @ivar pipeline: The current pipeline
    @type pipeline: L{Pipeline}
    @ivar action: The action controlled by this Pipeline
    @type action: L{ViewAction}
    """
    __gtype_name__ = 'PitiviViewer'
    __gsignals__ = {
        "activate-playback-controls": (GObject.SignalFlags.RUN_LAST,
            None, (GObject.TYPE_BOOLEAN,)),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app, undock_action=None):
        Gtk.VBox.__init__(self)
        self.set_border_width(SPACING)
        self.app = app
        self.settings = app.settings
        self.system = app.system

        Loggable.__init__(self)
        self.log("New PitiviViewer")

        self.pipeline = None
        self._tmp_pipeline = None  # Used for displaying a preview when trimming

        self.sink = None
        self.docked = True

        # Only used for restoring the pipeline position after a live clip trim preview:
        self._oldTimelinePos = None

        self._haveUI = False

        self._createUi()
        self.target = self.internal
        self.undock_action = undock_action
        if undock_action:
            self.undock_action.connect("activate", self._toggleDocked)

            if not self.settings.viewerDocked:
                self.undock()

    def setPipeline(self, pipeline, position=None):
        """
        Set the Viewer to the given Pipeline.

        Properly switches the currently set action to that new Pipeline.

        @param pipeline: The Pipeline to switch to.
        @type pipeline: L{Pipeline}.
        @param position: Optional position to seek to initially.
        """
        self.debug("self.pipeline:%r", self.pipeline)

        self.seeker = Seeker()
        self._disconnectFromPipeline()
        if self.pipeline:
            self.pipeline.set_state(Gst.State.NULL)

        self.pipeline = pipeline
        if self.pipeline:
            self.pipeline.pause()
            self.seeker.seek(position)

            self.pipeline.connect("state-change", self._pipelineStateChangedCb)
            self.pipeline.connect("position", self._positionCb)
            self.pipeline.connect("window-handle-message", self._windowHandleMessageCb)
            self.pipeline.connect("duration-changed", self._durationChangedCb)

        self._setUiActive()

    def _disconnectFromPipeline(self):
        self.debug("pipeline:%r", self.pipeline)
        if self.pipeline is None:
            # silently return, there's nothing to disconnect from
            return

        self.pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        self.pipeline.disconnect_by_func(self._windowHandleMessageCb)
        self.pipeline.disconnect_by_func(self._positionCb)
        self.pipeline.disconnect_by_func(self._durationChangedCb)

        self.pipeline = None

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        self.set_sensitive(active)
        if self._haveUI:
            for item in [self.goToStart_button, self.back_button,
                         self.playpause_button, self.forward_button,
                         self.goToEnd_button, self.timecode_entry]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, window, event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _createUi(self):
        """ Creates the Viewer GUI """
        # Drawing area
        # The aspect ratio gets overridden on startup by setDisplayAspectRatio
        self.aframe = Gtk.AspectFrame(xalign=0.5, yalign=1.0, ratio=4.0 / 3.0,
                                      obey_child=False)

        self.internal = ViewerWidget(self.app.settings)
        self.internal.init_transformation_events()
        self.internal.show()
        self.aframe.add(self.internal)
        self.pack_start(self.aframe, True, True, 0)

        self.external_window = Gtk.Window()
        vbox = Gtk.VBox()
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external = ViewerWidget(self.app.settings)
        vbox.pack_start(self.external, True, True, 0)
        self.external_window.connect("delete-event", self._externalWindowDeleteCb)
        self.external_window.connect("configure-event", self._externalWindowConfigureCb)
        self.external_vbox = vbox
        self.external_vbox.show_all()

        # Buttons/Controls
        bbox = Gtk.HBox()
        boxalign = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0.0)
        boxalign.add(bbox)
        self.pack_start(boxalign, False, True, 0)

        self.goToStart_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_PREVIOUS)
        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_tooltip_text(_("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, True, 0)

        self.back_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_REWIND)
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, True, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, True, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_FORWARD)
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, True, 0)

        self.goToEnd_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_NEXT)
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(_("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, True, 0)

        # current time
        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(_('Enter a timecode or frame number\nand press "Enter" to go to that position'))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        self.timecode_entry.connectFocusEvents(self._entryFocusInCb, self._entryFocusOutCb)
        bbox.pack_start(self.timecode_entry, False, 10, 0)
        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")

        screen = Gdk.Screen.get_default()
        height = screen.get_height()
        if height >= 800:
            # show the controls and force the aspect frame to have at least the same
            # width (+110, which is a magic number to minimize dead padding).
            bbox.show_all()
            req = bbox.size_request()
            width = req.width
            height = req.height
            width += 110
            height = int(width / self.aframe.props.ratio)
            self.aframe.set_size_request(width, height)
        self.show_all()
        self.buttons = bbox
        self.buttons_container = boxalign

    def setDisplayAspectRatio(self, ratio):
        """
        Sets the DAR of the Viewer to the given ratio.

        @arg ratio: The aspect ratio to set on the viewer
        @type ratio: L{float}
        """
        self.debug("Setting ratio of %f [%r]", float(ratio), ratio)
        try:
            self.aframe.set_property("ratio", float(ratio))
        except:
            self.warning("could not set ratio !")

    def _entryActivateCb(self, entry):
        self._seekFromTimecodeWidget()

    def _entryFocusInCb(self, entry, event):
        self.app.gui.setActionsSensitive(False)

    def _entryFocusOutCb(self, entry, event):
        self._seekFromTimecodeWidget()
        self.app.gui.setActionsSensitive(True)

    def _seekFromTimecodeWidget(self):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.seeker.seek(nanoseconds)

    ## active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        if duration == 0:
            self._setUiActive(False)
        else:
            self._setUiActive(True)

    ## Control Gtk.Button callbacks

    def setZoom(self, zoom):
        """
        Zoom in or out of the transformation box canvas.
        This is called by clipproperties.
        """
        if self.target.box:
            maxSize = self.target.area
            width = int(float(maxSize.width) * zoom)
            height = int(float(maxSize.height) * zoom)
            area = ((maxSize.width - width) / 2,
                    (maxSize.height - height) / 2,
                    width, height)
            self.sink.set_render_rectangle(*area)
            self.target.box.update_size(area)
            self.target.zoom = zoom
            self.target.sink = self.sink
            self.target.renderbox()

    def _playButtonCb(self, unused_button, playing):
        self.app.current.pipeline.togglePlayback()

    def _goToStartCb(self, unused_button):
        self.seeker.seek(0)

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.seeker.seekRelative(0 - Gst.SECOND)

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.seeker.seekRelative(Gst.SECOND)

    def _goToEndCb(self, unused_button):
        try:
            end = self.app.current.pipeline.getDuration()
        except:
            self.warning("Couldn't get timeline duration")
        try:
            self.seeker.seek(end)
        except:
            self.warning("Couldn't seek to the end of the timeline")

    ## public methods for controlling playback

    def undock(self):
        if not self.undock_action:
            self.error("Cannot undock because undock_action is missing.")
            return
        if not self.docked:
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.undock_action.set_label(_("Dock Viewer"))
        self.target = self.external

        self.remove(self.buttons_container)
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)
        self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
        self.external_window.show()

        self.fullscreen_button = Gtk.ToggleToolButton(Gtk.STOCK_FULLSCREEN)
        self.fullscreen_button.set_tooltip_text(_("Show this window in fullscreen"))
        self.buttons.pack_end(self.fullscreen_button, expand=False, fill=False, padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggleFullscreen)

        # if we are playing, switch output immediately
        if self.sink:
            self._switch_output_window()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(self.settings.viewerWidth, self.settings.viewerHeight)

    def dock(self):
        if not self.undock_action:
            self.error("Cannot dock because undock_action is missing.")
            return
        if self.docked:
            return
        self.docked = True
        self.settings.viewerDocked = True
        self.undock_action.set_label(_("Undock Viewer"))
        self.target = self.internal

        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()
        # if we are playing, switch output immediately
        if self.sink:
            self._switch_output_window()
        self.external_window.hide()

    def _toggleDocked(self, action):
        if self.docked:
            self.undock()
        else:
            self.dock()

    def _toggleFullscreen(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """
        If the timeline position changed, update the viewer UI widgets.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, tl_obj, position):
        """
        While a clip is being trimmed, show a live preview of it.
        """
        if isinstance(tl_obj, GES.TitleClip) or tl_obj.props.is_image or not hasattr(tl_obj, "get_uri"):
            self.log("%s is an image or has no URI, so not previewing trim" % tl_obj)
            return False

        clip_uri = tl_obj.props.uri
        cur_time = time()
        if not self._tmp_pipeline:
            self.debug("Creating temporary pipeline for clip %s, position %s",
                clip_uri, print_ns(position))

            self._oldTimelinePos = self.pipeline.getPosition()
            self._tmp_pipeline = Gst.ElementFactory.make("playbin", None)
            self._tmp_pipeline.set_property("uri", clip_uri)
            self.setPipeline(SimplePipeline(self._tmp_pipeline))
            self._lastClipTrimTime = cur_time
        if (cur_time - self._lastClipTrimTime) > 0.2:
            # Do not seek more than once every 200 ms (for performance)
            self._tmp_pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)
            self._lastClipTrimTime = cur_time

    def clipTrimPreviewFinished(self):
        """
        After trimming a clip, reset the project pipeline into the viewer.
        """
        if self._tmp_pipeline is not None:
            self._tmp_pipeline.set_state(Gst.State.NULL)
            self._tmp_pipeline = None  # Free the memory
            self.setPipeline(self.app.current.pipeline, self._oldTimelinePos)
            self.debug("Back to old pipeline")

    def _pipelineStateChangedCb(self, pipeline, state):
        """
        When playback starts/stops, update the viewer widget,
        play/pause button and (un)inhibit the screensaver.

        This is meant to be called by mainwindow.
        """
        self.info("current state changed : %s", state)
        if int(state) == int(Gst.State.PLAYING):
            self.playpause_button.setPause()
            self.system.inhibitScreensaver(self.INHIBIT_REASON)
        elif int(state) == int(Gst.State.PAUSED):
            self.playpause_button.setPlay()
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        else:
            self.sink = None
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        self.internal._currentStateCb(self.pipeline, state)

    def _windowHandleMessageCb(self, unused_pipeline, message):
        """
        When the pipeline sends us a message to prepare-xwindow-id,
        tell the viewer to switch its output window.
        """
        self.sink = message.src
        self._switch_output_window()

    def _switch_output_window(self):
        Gdk.threads_enter()
        # Prevent cases where target has no "window_xid" (yes, it happens!):
        self.target.show()
        self.sink.set_window_handle(self.target.window_xid)
        self.sink.expose()
        Gdk.threads_leave()
Ejemplo n.º 3
0
class ViewerContainer(Gtk.Box, Loggable):
    """Wiget holding a viewer and the controls.

    Attributes:
        pipeline (SimplePipeline): The displayed pipeline.
    """

    __gtype_name__ = 'ViewerContainer'
    __gsignals__ = {
        "activate-playback-controls":
        (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_BOOLEAN, )),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app):
        Gtk.Box.__init__(self)
        self.app = app
        self.settings = app.settings

        Loggable.__init__(self)
        self.log("New ViewerContainer")

        self.project = None
        self.trim_pipeline = None
        self.trim_pipelines_cache = collections.OrderedDict()
        self.docked = True
        self.target = None
        self._compactMode = False

        self._haveUI = False

        self._createUi()

        if not self.settings.viewerDocked:
            self.undock()

        self.__cursor = None
        self.__translation = None

        pm = self.app.project_manager
        pm.connect("new-project-loaded",
                   self._project_manager_new_project_loaded_cb)
        pm.connect("project-closed", self._projectManagerProjectClosedCb)

    def _project_manager_new_project_loaded_cb(self, unused_project_manager,
                                               project):
        project.connect("rendering-settings-changed",
                        self._project_rendering_settings_changed_cb)
        self.set_project(project)

    def _projectManagerProjectClosedCb(self, unused_project_manager, project):
        if self.project == project:
            project.disconnect_by_func(
                self._project_rendering_settings_changed_cb)
        self.project = None

    def _project_rendering_settings_changed_cb(self, project, unused_item):
        """Handles Project metadata changes."""
        self._reset_viewer_aspect_ratio(project)

    def _reset_viewer_aspect_ratio(self, project):
        """Resets the viewer aspect ratio."""
        self.target.update_aspect_ratio(project)
        self.timecode_entry.setFramerate(project.videorate)

    def set_project(self, project):
        """Sets the displayed project.

        Args:
            project (Project): The Project to switch to.
        """
        self.debug("Setting project: %r", project)
        self._disconnectFromPipeline()

        if self.target:
            parent = self.target.get_parent()
            if parent:
                parent.remove(self.target)

        project.pipeline.connect("state-change", self._pipelineStateChangedCb)
        project.pipeline.connect("position", self._positionCb)
        project.pipeline.connect("duration-changed", self._durationChangedCb)
        self.project = project

        self.__createNewViewer()
        self._setUiActive()

        # This must be done at the end, otherwise the created sink widget
        # appears in a separate window.
        project.pipeline.pause()

    def __createNewViewer(self):
        _, sink_widget = self.project.pipeline.create_sink()

        self.overlay_stack = OverlayStack(self.app, sink_widget)
        self.target = ViewerWidget(self.overlay_stack)
        self._reset_viewer_aspect_ratio(self.project)

        if self.docked:
            self.pack_start(self.target, expand=True, fill=True, padding=0)
        else:
            self.external_vbox.pack_start(self.target,
                                          expand=True,
                                          fill=False,
                                          padding=0)
            self.external_vbox.child_set(self.target, fill=True)

        self.target.show_all()

        # Wait for 1s to make sure that the viewer has completely realized
        # and then we can mark the resize status as showable.
        GLib.timeout_add(1000, self.__viewer_realization_done_cb, None)

    def _disconnectFromPipeline(self):
        if self.project is None:
            return

        pipeline = self.project.pipeline
        self.debug("Disconnecting from: %r", pipeline)
        pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        pipeline.disconnect_by_func(self._positionCb)
        pipeline.disconnect_by_func(self._durationChangedCb)

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        if self._haveUI:
            for item in [
                    self.goToStart_button, self.back_button,
                    self.playpause_button, self.forward_button,
                    self.goToEnd_button, self.timecode_entry
            ]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, unused_window, unused_event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, unused_window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _createUi(self):
        """Creates the Viewer GUI."""
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self.external_window = Gtk.Window()
        vbox = Gtk.Box()
        vbox.set_orientation(Gtk.Orientation.VERTICAL)
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external_window.connect("delete-event",
                                     self._externalWindowDeleteCb)
        self.external_window.connect("configure-event",
                                     self._externalWindowConfigureCb)
        self.external_vbox = vbox

        # Corner marker.
        corner = Gtk.DrawingArea()
        # Number of lines to draw in the corner marker.
        lines = 3
        # Space between each line.
        space = 5
        # Margin from left and bottom of viewer container.
        margin = 2
        corner_size = space * lines + margin
        corner.set_size_request(corner_size, corner_size)
        corner.set_halign(Gtk.Align.START)
        corner.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK
                          | Gdk.EventMask.BUTTON_PRESS_MASK
                          | Gdk.EventMask.BUTTON_RELEASE_MASK
                          | Gdk.EventMask.POINTER_MOTION_MASK)
        hpane = self.app.gui.editor.mainhpaned
        vpane = self.app.gui.editor.toplevel_widget
        corner.connect("draw", self.__corner_draw_cb, lines, space, margin)
        corner.connect("enter-notify-event", self.__corner_enter_notify_cb)
        corner.connect("button-press-event", self.__corner_button_press_cb,
                       hpane, vpane)
        corner.connect("button-release-event", self.__corner_button_release_cb)
        corner.connect("motion-notify-event", self.__corner_motion_notify_cb,
                       hpane, vpane)
        self.pack_end(corner, False, False, 0)

        # Buttons/Controls
        bbox = Gtk.Box()
        bbox.set_orientation(Gtk.Orientation.HORIZONTAL)
        bbox.set_property("valign", Gtk.Align.CENTER)
        bbox.set_property("halign", Gtk.Align.CENTER)
        bbox.set_margin_left(SPACING)
        bbox.set_margin_right(SPACING)
        self.pack_end(bbox, False, False, 0)

        self.goToStart_button = Gtk.Button.new_from_icon_name(
            "media-skip-backward-symbolic", Gtk.IconSize.BUTTON)

        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_relief(Gtk.ReliefStyle.NONE)
        self.goToStart_button.set_tooltip_text(
            _("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, False, 0)

        self.back_button = Gtk.Button.new_from_icon_name(
            "media-seek-backward-symbolic", Gtk.IconSize.BUTTON)

        self.back_button.set_relief(Gtk.ReliefStyle.NONE)
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, False, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, False, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.Button.new_from_icon_name(
            "media-seek-forward-symbolic", Gtk.IconSize.BUTTON)
        self.forward_button.set_relief(Gtk.ReliefStyle.NONE)
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, False, 0)

        self.goToEnd_button = Gtk.Button.new_from_icon_name(
            "media-skip-forward-symbolic", Gtk.IconSize.BUTTON)
        self.goToEnd_button.set_relief(Gtk.ReliefStyle.NONE)
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(
            _("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, False, 0)

        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(
            _('Enter a timecode or frame number\nand press "Enter" to go to that position'
              ))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        self.timecode_entry.connect("key_press_event",
                                    self._entry_key_press_event_cb)
        bbox.pack_start(self.timecode_entry, False, False, 15)

        self.undock_button = Gtk.Button.new_from_icon_name(
            "view-restore-symbolic", Gtk.IconSize.BUTTON)

        self.undock_button.set_relief(Gtk.ReliefStyle.NONE)
        self.undock_button.connect("clicked", self.undock_cb)
        self.undock_button.set_tooltip_text(
            _("Detach the viewer\nYou can re-attach it by closing the newly created window."
              ))
        bbox.pack_start(self.undock_button, False, False, 0)

        self.show_all()

        # Create a hidden container for the clip trim preview video widget.
        self.hidden_chest = Gtk.Frame()
        # It has to be added to the window, otherwise when we add
        # a video widget to it, it will create a new window!
        self.pack_end(self.hidden_chest, False, False, 0)

        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")
        self.undock_button.get_accessible().set_name("undock_button")

        self.buttons_container = bbox
        self.external_vbox.show_all()

    def __corner_draw_cb(self, unused_widget, cr, lines, space, margin):
        cr.set_line_width(1)

        marker_color = self.app.gui.get_style_context().lookup_color("borders")
        cr.set_source_rgb(marker_color.color.red, marker_color.color.green,
                          marker_color.color.blue)

        cr.translate(margin, 0)
        for i in range(lines):
            cr.move_to(0, space * i)
            cr.line_to(space * (lines - i), space * lines)
            cr.stroke()

    def __corner_enter_notify_cb(self, widget, unused_event):
        if not self.__cursor:
            self.__cursor = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_LEFT_CORNER)
        widget.get_window().set_cursor(self.__cursor)

    def __corner_button_press_cb(self, unused_widget, event, hpane, vpane):
        if event.button == 1:
            # The mouse pointer position is w.r.t the root of the screen
            # whereas the positions of panes is w.r.t the root of the
            # mainwindow. We need to find the translation that takes us
            # from screen coordinate system to mainwindow coordinate system.
            self.__translation = (event.x_root - hpane.get_position(),
                                  event.y_root - vpane.get_position())

    def __corner_button_release_cb(self, unused_widget, unused_event):
        self.__translation = None

    def __corner_motion_notify_cb(self, unused_widget, event, hpane, vpane):
        if self.__translation is None:
            return

        hpane.set_position(event.x_root - self.__translation[0])
        vpane.set_position(event.y_root - self.__translation[1])

    def activateCompactMode(self):
        self.back_button.hide()
        self.forward_button.hide()
        self._compactMode = True  # Prevent set_size_request later

    def _entryActivateCb(self, unused_entry):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.app.project_manager.current_project.pipeline.simple_seek(
            nanoseconds)
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.CENTER, when_not_in_view=True)

    def _entry_key_press_event_cb(self, widget, event):
        """Handles the key press events in the timecode_entry widget."""
        if event.keyval == Gdk.KEY_Escape:
            self.app.gui.editor.focusTimeline()

    # Active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        self._setUiActive(duration > 0)

    def _playButtonCb(self, unused_button, unused_playing):
        self.app.project_manager.current_project.pipeline.togglePlayback()
        self.app.gui.editor.focusTimeline()

    def _goToStartCb(self, unused_button):
        self.app.project_manager.current_project.pipeline.simple_seek(0)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.START, when_not_in_view=True)

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.app.project_manager.current_project.pipeline.seekRelative(
            0 - Gst.SECOND)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.END, when_not_in_view=True)

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.app.project_manager.current_project.pipeline.seekRelative(
            Gst.SECOND)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.START, when_not_in_view=True)

    def _goToEndCb(self, unused_button):
        end = self.app.project_manager.current_project.pipeline.getDuration()
        self.app.project_manager.current_project.pipeline.simple_seek(end)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.CENTER, when_not_in_view=True)

    def undock_cb(self, unused_widget):
        self.undock()

    # Public methods for controlling playback

    def undock(self):
        if not self.docked:
            self.warning("The viewer is already undocked")
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.remove(self.buttons_container)
        position = None
        if self.project:
            self.overlay_stack.enable_resize_status(False)
            position = self.project.pipeline.getPosition()
            self.project.pipeline.setState(Gst.State.NULL)
            self.remove(self.target)
            self.__createNewViewer()
        self.buttons_container.set_margin_bottom(SPACING)
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)

        self.undock_button.hide()
        self.fullscreen_button = Gtk.ToggleButton()
        fullscreen_image = Gtk.Image.new_from_icon_name(
            "view-fullscreen-symbolic", Gtk.IconSize.BUTTON)
        self.fullscreen_button.set_image(fullscreen_image)
        self.fullscreen_button.set_tooltip_text(
            _("Show this window in fullscreen"))
        self.fullscreen_button.set_relief(Gtk.ReliefStyle.NONE)
        self.buttons_container.pack_end(self.fullscreen_button,
                                        expand=False,
                                        fill=False,
                                        padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggle_fullscreen_cb)

        self.external_window.show()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(self.settings.viewerWidth,
                                    self.settings.viewerHeight)
        if self.project:
            self.project.pipeline.pause()
            self.project.pipeline.simple_seek(position)

    def __viewer_realization_done_cb(self, unused_data):
        self.overlay_stack.enable_resize_status(True)
        return False

    def dock(self):
        if self.docked:
            self.warning("The viewer is already docked")
            return

        self.docked = True
        self.settings.viewerDocked = True

        position = None
        if self.project:
            self.overlay_stack.enable_resize_status(False)
            position = self.project.pipeline.getPosition()
            self.project.pipeline.setState(Gst.State.NULL)
            self.external_vbox.remove(self.target)
            self.__createNewViewer()

        self.undock_button.show()
        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.buttons_container.set_margin_bottom(0)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()

        self.external_window.hide()
        if self.project.pipeline:
            self.project.pipeline.pause()
            self.project.pipeline.simple_seek(position)

    def _toggle_fullscreen_cb(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """Updates the viewer UI widgets if the timeline position changed.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, clip, position):
        """Shows a live preview of a clip being trimmed."""
        if not hasattr(clip, "get_uri") or isinstance(
                clip, GES.TitleClip) or clip.props.is_image:
            self.log("Not previewing trim for image or title clip: %s", clip)
            return False

        if self.project.pipeline.getState() == Gst.State.PLAYING:
            self.project.pipeline.setState(Gst.State.PAUSED)

        uri = clip.props.uri
        if self.trim_pipeline and uri != self.trim_pipeline.uri:
            # Seems to be the trim preview pipeline for a different clip.
            self.trim_pipeline.release()
            self.trim_pipeline = None

        if not self.trim_pipeline:
            self.trim_pipeline, sink_widget = self.get_trim_preview_pipeline(
                uri)
            # Add the widget to a hidden container and make it appear later
            # when it's ready. If we show it before the initial seek completion,
            # there is a flicker when the first frame of the asset is shown for
            # a brief moment until the initial seek to the frame we actually
            # want to show is performed.
            # First make sure the container itself is ready.
            widget = self.hidden_chest.get_child()
            if widget:
                self.warning(
                    "The previous trim preview video widget should have been removed already"
                )
                self.hidden_chest.remove(widget)
            self.hidden_chest.add(sink_widget)
            sink_widget.show()
            self.trim_pipeline.connect("state-change", self._state_change_cb)
            self.trim_pipeline.setState(Gst.State.PAUSED)
            self._last_trim_ns = 0

        self.trim_pipeline.simple_seek(position)

    def get_trim_preview_pipeline(self, uri):
        try:
            trim_pipeline, sink_widget = self.trim_pipelines_cache[uri]
            self.debug("Reusing temporary pipeline for clip %s", uri)
        except KeyError:
            self.debug("Creating temporary pipeline for clip %s", uri)
            trim_pipeline = AssetPipeline(uri)
            unused_video_sink, sink_widget = trim_pipeline.create_sink()
        self.trim_pipelines_cache[uri] = trim_pipeline, sink_widget
        if len(self.trim_pipelines_cache) > 4:
            # Pop the first inserted item.
            expired_uri, (
                expired_pipeline,
                unused_expired_widget) = self.trim_pipelines_cache.popitem(
                    last=False)
            self.debug("Releasing temporary pipeline for clip %s", expired_uri)
            expired_pipeline.release()
        return trim_pipeline, sink_widget

    def _state_change_cb(self, trim_pipeline, state, prev_state):
        if self.trim_pipeline is not trim_pipeline:
            self.warning(
                "State change reported for previous trim preview pipeline")
            trim_pipeline.disconnect_by_func(self._state_change_cb)
            return
        # First the pipeline goes from READY to PAUSED, and then it goes
        # from PAUSED to PAUSED, and this is a good moment.
        if prev_state == Gst.State.PAUSED and state == Gst.State.PAUSED:
            sink_widget = self.hidden_chest.get_child()
            if sink_widget:
                self.hidden_chest.remove(sink_widget)
                self.target.switch_widget(sink_widget)
            trim_pipeline.disconnect_by_func(self._state_change_cb)

    def clipTrimPreviewFinished(self):
        """Switches back to the project pipeline following a clip trimming."""
        if not self.trim_pipeline:
            return
        self.target.switch_widget(self.overlay_stack)
        self.trim_pipeline = None

    def _pipelineStateChangedCb(self, pipeline, state, old_state):
        """Updates the widgets when the playback starts or stops."""
        if state == Gst.State.PLAYING:
            st = Gst.Structure.new_empty("play")
            self.app.write_action(st)
            self.playpause_button.setPause()
            self.app.simple_inhibit(ViewerContainer.INHIBIT_REASON,
                                    Gtk.ApplicationInhibitFlags.IDLE)
            self.overlay_stack.hide_overlays()
        else:
            if state == Gst.State.PAUSED:
                if old_state != Gst.State.PAUSED:
                    st = Gst.Structure.new_empty("pause")
                    if old_state == Gst.State.PLAYING:
                        position_seconds = pipeline.getPosition() / Gst.SECOND
                        st.set_value("playback_time", position_seconds)
                    self.app.write_action(st)

                self.playpause_button.setPlay()
            self.overlay_stack.show_overlays()
            self.app.simple_uninhibit(ViewerContainer.INHIBIT_REASON)
Ejemplo n.º 4
0
class ViewerContainer(Gtk.Box, Loggable):
    """Wiget holding a viewer and the controls.

    Attributes:
        pipeline (SimplePipeline): The displayed pipeline.
    """

    __gtype_name__ = 'ViewerContainer'
    __gsignals__ = {
        "activate-playback-controls":
        (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_BOOLEAN, )),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app):
        Gtk.Box.__init__(self)
        self.set_border_width(SPACING)
        self.app = app
        self.settings = app.settings

        Loggable.__init__(self)
        self.log("New ViewerContainer")

        self.pipeline = None
        self.docked = True
        self.target = None
        self._compactMode = False

        # Only used for restoring the pipeline position after a live clip trim
        # preview:
        self._oldTimelinePos = None

        self._haveUI = False

        self._createUi()

        self.__owning_pipeline = False
        if not self.settings.viewerDocked:
            self.undock()

    def setPipeline(self, pipeline, position=None):
        """Sets the displayed pipeline.

        Properly switches the currently set action to that new Pipeline.

        Args:
            pipeline (Pipeline): The Pipeline to switch to.
            position (Optional[int]): The position to seek to initially.
        """
        self.debug("Setting pipeline: %r", pipeline)
        self._disconnectFromPipeline()

        if self.target:
            parent = self.target.get_parent()
            if parent:
                parent.remove(self.target)

        self.pipeline = pipeline
        self.pipeline.connect("state-change", self._pipelineStateChangedCb)
        self.pipeline.connect("position", self._positionCb)
        self.pipeline.connect("duration-changed", self._durationChangedCb)

        self.__owning_pipeline = False
        self.__createNewViewer()
        self._setUiActive()

        if position:
            self.pipeline.simple_seek(position)
        self.pipeline.pause()

    def __createNewViewer(self):
        self.pipeline.create_sink()

        self.overlay_stack = OverlayStack(self.app, self.pipeline.sink_widget)
        self.target = ViewerWidget(self.overlay_stack)

        if self.docked:
            self.pack_start(self.target, True, True, 0)
            # Force the AspectFrame to be tall (and wide) enough to look good.
            # TODO: review this code to create a smarter algorithm.
            if not self._compactMode:
                req = self.buttons.get_preferred_size()[0]
                width = req.width
                height = int(width / self.target.props.ratio)
                width += 110  # Magic number to minimize dead padding
                self.target.set_size_request(width, height)
        else:
            self.external_vbox.pack_start(self.target, False, False, 0)
            self.target.props.expand = True
            self.external_vbox.child_set(self.target, fill=True)

        self.setDisplayAspectRatio(
            self.app.project_manager.current_project.getDAR())
        self.target.show_all()

    def _disconnectFromPipeline(self):
        if self.pipeline is None:
            # silently return, there's nothing to disconnect from
            return

        self.debug("Disconnecting from: %r", self.pipeline)
        self.pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        self.pipeline.disconnect_by_func(self._positionCb)
        self.pipeline.disconnect_by_func(self._durationChangedCb)

        if self.__owning_pipeline:
            self.pipeline.release()
        self.pipeline = None

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        self.set_sensitive(active)
        if self._haveUI:
            for item in [
                    self.goToStart_button, self.back_button,
                    self.playpause_button, self.forward_button,
                    self.goToEnd_button, self.timecode_entry
            ]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, unused_window, unused_event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, unused_window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _createUi(self):
        """Creates the Viewer GUI."""
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self.external_window = Gtk.Window()
        vbox = Gtk.Box()
        vbox.set_orientation(Gtk.Orientation.VERTICAL)
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external_window.connect("delete-event",
                                     self._externalWindowDeleteCb)
        self.external_window.connect("configure-event",
                                     self._externalWindowConfigureCb)
        self.external_vbox = vbox

        # Buttons/Controls
        bbox = Gtk.Box()
        bbox.set_orientation(Gtk.Orientation.HORIZONTAL)
        bbox.set_property("valign", Gtk.Align.CENTER)
        bbox.set_property("halign", Gtk.Align.CENTER)
        self.pack_end(bbox, False, False, SPACING)

        self.goToStart_button = Gtk.ToolButton()
        self.goToStart_button.set_icon_name("media-skip-backward")
        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_tooltip_text(
            _("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, False, 0)

        self.back_button = Gtk.ToolButton()
        self.back_button.set_icon_name("media-seek-backward")
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, False, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, False, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.ToolButton()
        self.forward_button.set_icon_name("media-seek-forward")
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, False, 0)

        self.goToEnd_button = Gtk.ToolButton()
        self.goToEnd_button.set_icon_name("media-skip-forward")
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(
            _("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, False, 0)

        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(
            _('Enter a timecode or frame number\nand press "Enter" to go to that position'
              ))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        self.timecode_entry.connect("key_press_event",
                                    self._entry_key_press_event_cb)
        bbox.pack_start(self.timecode_entry, False, 10, 0)

        self.undock_button = Gtk.ToolButton()
        self.undock_button.set_icon_name("view-restore")
        self.undock_button.connect("clicked", self.undock)
        self.undock_button.set_tooltip_text(
            _("Detach the viewer\nYou can re-attach it by closing the newly created window."
              ))
        bbox.pack_start(self.undock_button, False, False, 0)

        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")
        self.undock_button.get_accessible().set_name("undock_button")

        self.buttons = bbox
        self.buttons_container = bbox
        self.show_all()
        self.external_vbox.show_all()

    def activateCompactMode(self):
        self.back_button.hide()
        self.forward_button.hide()
        self._compactMode = True  # Prevent set_size_request later

    def setDisplayAspectRatio(self, ratio):
        self.debug("Setting aspect ratio to %f [%r]", float(ratio), ratio)
        self.target.setDisplayAspectRatio(ratio)

    def _entryActivateCb(self, unused_entry):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.app.project_manager.current_project.pipeline.simple_seek(
            nanoseconds)
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.CENTER, when_not_in_view=True)

    def _entry_key_press_event_cb(self, widget, event):
        """Handles the key press events in the timecode_entry widget."""
        if event.keyval == Gdk.KEY_Escape:
            self.app.gui.editor.focusTimeline()

    # Active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        if duration == 0:
            self._setUiActive(False)
        else:
            self._setUiActive(True)

    def _playButtonCb(self, unused_button, unused_playing):
        self.app.project_manager.current_project.pipeline.togglePlayback()
        self.app.gui.editor.focusTimeline()

    def _goToStartCb(self, unused_button):
        self.app.project_manager.current_project.pipeline.simple_seek(0)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.START, when_not_in_view=True)

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.app.project_manager.current_project.pipeline.seekRelative(
            0 - Gst.SECOND)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.END, when_not_in_view=True)

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.app.project_manager.current_project.pipeline.seekRelative(
            Gst.SECOND)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.START, when_not_in_view=True)

    def _goToEndCb(self, unused_button):
        end = self.app.project_manager.current_project.pipeline.getDuration()
        self.app.project_manager.current_project.pipeline.simple_seek(end)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.CENTER, when_not_in_view=True)

    # Public methods for controlling playback

    def undock(self, *unused_widget):
        if not self.docked:
            self.warning("The viewer is already undocked")
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.remove(self.buttons_container)
        position = None
        if self.pipeline:
            position = self.pipeline.getPosition()
            self.pipeline.setState(Gst.State.NULL)
            self.remove(self.target)
            self.__createNewViewer()
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)

        self.undock_button.hide()
        self.fullscreen_button = Gtk.ToggleToolButton()
        self.fullscreen_button.set_icon_name("view-fullscreen")
        self.fullscreen_button.set_tooltip_text(
            _("Show this window in fullscreen"))
        self.buttons.pack_end(self.fullscreen_button,
                              expand=False,
                              fill=False,
                              padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggleFullscreen)

        self.external_window.show()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(self.settings.viewerWidth,
                                    self.settings.viewerHeight)
        if self.pipeline:
            self.pipeline.pause()
            self.pipeline.simple_seek(position)

    def dock(self):
        if self.docked:
            self.warning("The viewer is already docked")
            return

        self.docked = True
        self.settings.viewerDocked = True

        position = None
        if self.pipeline:
            position = self.pipeline.getPosition()
            self.pipeline.setState(Gst.State.NULL)
            self.external_vbox.remove(self.target)
            self.__createNewViewer()

        self.undock_button.show()
        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()

        self.external_window.hide()
        if self.pipeline:
            self.pipeline.pause()
            self.pipeline.simple_seek(position)

    def _toggleFullscreen(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """Updates the viewer UI widgets if the timeline position changed.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, clip, position):
        """Shows a live preview of a clip being trimmed."""
        if not hasattr(clip, "get_uri") or isinstance(
                clip, GES.TitleClip) or clip.props.is_image:
            self.log("%s is an image or has no URI, so not previewing trim" %
                     clip)
            return False

        clip_uri = clip.props.uri
        cur_time = time()
        if self.pipeline == self.app.project_manager.current_project.pipeline:
            self.debug("Creating temporary pipeline for clip %s, position %s",
                       clip_uri, format_ns(position))
            self._oldTimelinePos = self.pipeline.getPosition(False)
            self.pipeline.set_state(Gst.State.NULL)
            self.setPipeline(AssetPipeline(clip))
            self.__owning_pipeline = True
            self._lastClipTrimTime = cur_time

        if (cur_time - self._lastClipTrimTime
            ) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED:
            # Do not seek more than once every 200 ms (for performance)
            self.pipeline.simple_seek(position)
            self._lastClipTrimTime = cur_time

    def clipTrimPreviewFinished(self):
        """Switches back to the project pipeline following a clip trimming."""
        if self.pipeline is not self.app.project_manager.current_project.pipeline:
            self.debug("Going back to the project's pipeline")
            self.pipeline.setState(Gst.State.NULL)
            # Using pipeline.getPosition() here does not work because for some
            # reason it's a bit off, that's why we need self._oldTimelinePos.
            self.setPipeline(self.app.project_manager.current_project.pipeline,
                             self._oldTimelinePos)
            self._oldTimelinePos = None

    def _pipelineStateChangedCb(self, unused_pipeline, state, old_state):
        """Updates the widgets when the playback starts or stops."""
        if state == Gst.State.PLAYING:
            st = Gst.Structure.new_empty("play")
            self.app.write_action(st)
            self.playpause_button.setPause()
            self.app.simple_inhibit(ViewerContainer.INHIBIT_REASON,
                                    Gtk.ApplicationInhibitFlags.IDLE)
            self.overlay_stack.hide_overlays()
        else:
            if state == Gst.State.PAUSED:
                if old_state != Gst.State.PAUSED:
                    st = Gst.Structure.new_empty("pause")
                    if old_state == Gst.State.PLAYING:
                        st.set_value("playback_time",
                                     self.pipeline.getPosition() / Gst.SECOND)
                    self.app.write_action(st)

                self.playpause_button.setPlay()
            self.overlay_stack.show_overlays()
            self.app.simple_uninhibit(ViewerContainer.INHIBIT_REASON)
Ejemplo n.º 5
0
class ViewerContainer(Gtk.Box, Loggable):
    """Wiget holding a viewer and the controls.

    Attributes:
        pipeline (SimplePipeline): The displayed pipeline.
    """

    __gtype_name__ = 'ViewerContainer'
    __gsignals__ = {
        "activate-playback-controls": (GObject.SignalFlags.RUN_LAST,
                                       None, (GObject.TYPE_BOOLEAN,)),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app):
        Gtk.Box.__init__(self)
        self.set_border_width(SPACING)
        self.app = app
        self.settings = app.settings

        Loggable.__init__(self)
        self.log("New ViewerContainer")

        self.pipeline = None
        self.docked = True
        self.target = None
        self._compactMode = False

        # Only used for restoring the pipeline position after a live clip trim
        # preview:
        self._oldTimelinePos = None

        self._haveUI = False

        self._createUi()

        self.__owning_pipeline = False
        if not self.settings.viewerDocked:
            self.undock()

    def setPipeline(self, pipeline, position=None):
        """Sets the displayed pipeline.

        Properly switches the currently set action to that new Pipeline.

        Args:
            pipeline (Pipeline): The Pipeline to switch to.
            position (Optional[int]): The position to seek to initially.
        """
        self._disconnectFromPipeline()

        if self.target:
            parent = self.target.get_parent()
            if parent:
                parent.remove(self.target)

        self.debug("New pipeline: %r", pipeline)
        self.pipeline = pipeline
        if position:
            self.pipeline.simple_seek(position)

        self.pipeline.connect("state-change", self._pipelineStateChangedCb)
        self.pipeline.connect("position", self._positionCb)
        self.pipeline.connect("duration-changed", self._durationChangedCb)

        self.__owning_pipeline = False
        self.__createNewViewer()
        self._setUiActive()

        self.pipeline.pause()

    def __createNewViewer(self):
        self.pipeline.create_sink()

        self.overlay_stack = OverlayStack(self.app, self.pipeline.sink_widget)
        self.target = ViewerWidget(self.overlay_stack)

        if self.docked:
            self.pack_start(self.target, True, True, 0)
            screen = Gdk.Screen.get_default()
            height = screen.get_height()
            # Force the AspectFrame to be tall (and wide) enough to look good.
            # TODO: review this code to create a smarter algorithm.
            if not self._compactMode:
                req = self.buttons.get_preferred_size()[0]
                width = req.width
                height = int(width / self.target.props.ratio)
                width += 110  # Magic number to minimize dead padding
                self.target.set_size_request(width, height)
        else:
            self.external_vbox.pack_start(self.target, False, False, 0)
            self.target.props.expand = True
            self.external_vbox.child_set(self.target, fill=True)

        self.setDisplayAspectRatio(self.app.project_manager.current_project.getDAR())
        self.target.show_all()

    def _disconnectFromPipeline(self):
        self.debug("Previous pipeline: %r", self.pipeline)
        if self.pipeline is None:
            # silently return, there's nothing to disconnect from
            return

        self.pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        self.pipeline.disconnect_by_func(self._positionCb)
        self.pipeline.disconnect_by_func(self._durationChangedCb)

        if self.__owning_pipeline:
            self.pipeline.release()
        self.pipeline = None

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        self.set_sensitive(active)
        if self._haveUI:
            for item in [self.goToStart_button, self.back_button,
                         self.playpause_button, self.forward_button,
                         self.goToEnd_button, self.timecode_entry]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, unused_window, unused_event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, unused_window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _createUi(self):
        """Creates the Viewer GUI."""
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self.external_window = Gtk.Window()
        vbox = Gtk.Box()
        vbox.set_orientation(Gtk.Orientation.VERTICAL)
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external_window.connect(
            "delete-event", self._externalWindowDeleteCb)
        self.external_window.connect(
            "configure-event", self._externalWindowConfigureCb)
        self.external_vbox = vbox

        # Buttons/Controls
        bbox = Gtk.Box()
        bbox.set_orientation(Gtk.Orientation.HORIZONTAL)
        bbox.set_property("valign", Gtk.Align.CENTER)
        bbox.set_property("halign", Gtk.Align.CENTER)
        self.pack_end(bbox, False, False, SPACING)

        self.goToStart_button = Gtk.ToolButton()
        self.goToStart_button.set_icon_name("media-skip-backward")
        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_tooltip_text(
            _("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, False, 0)

        self.back_button = Gtk.ToolButton()
        self.back_button.set_icon_name("media-seek-backward")
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, False, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, False, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.ToolButton()
        self.forward_button.set_icon_name("media-seek-forward")
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, False, 0)

        self.goToEnd_button = Gtk.ToolButton()
        self.goToEnd_button.set_icon_name("media-skip-forward")
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(
            _("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, False, 0)

        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(
            _('Enter a timecode or frame number\nand press "Enter" to go to that position'))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        self.timecode_entry.connect("key_press_event", self._entry_key_press_event_cb)
        bbox.pack_start(self.timecode_entry, False, 10, 0)

        self.undock_button = Gtk.ToolButton()
        self.undock_button.set_icon_name("view-restore")
        self.undock_button.connect("clicked", self.undock)
        self.undock_button.set_tooltip_text(
            _("Detach the viewer\nYou can re-attach it by closing the newly created window."))
        bbox.pack_start(self.undock_button, False, False, 0)

        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")
        self.undock_button.get_accessible().set_name("undock_button")

        self.buttons = bbox
        self.buttons_container = bbox
        self.show_all()
        self.external_vbox.show_all()

    def activateCompactMode(self):
        self.back_button.hide()
        self.forward_button.hide()
        self._compactMode = True  # Prevent set_size_request later

    def setDisplayAspectRatio(self, ratio):
        self.debug("Setting aspect ratio to %f [%r]", float(ratio), ratio)
        self.target.setDisplayAspectRatio(ratio)

    def _entryActivateCb(self, unused_entry):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.app.project_manager.current_project.pipeline.simple_seek(nanoseconds)
        self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.CENTER, when_not_in_view=True)

    def _entry_key_press_event_cb(self, widget, event):
        """Handles the key press events in the timecode_entry widget."""
        if event.keyval == Gdk.KEY_Escape:
            self.app.gui.focusTimeline()

    # Active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        if duration == 0:
            self._setUiActive(False)
        else:
            self._setUiActive(True)

    def _playButtonCb(self, unused_button, unused_playing):
        self.app.project_manager.current_project.pipeline.togglePlayback()
        self.app.gui.focusTimeline()

    def _goToStartCb(self, unused_button):
        self.app.project_manager.current_project.pipeline.simple_seek(0)
        self.app.gui.focusTimeline()
        self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.START, when_not_in_view=True)

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.app.project_manager.current_project.pipeline.seekRelative(0 - Gst.SECOND)
        self.app.gui.focusTimeline()
        self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.END, when_not_in_view=True)

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.app.project_manager.current_project.pipeline.seekRelative(Gst.SECOND)
        self.app.gui.focusTimeline()
        self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.START, when_not_in_view=True)

    def _goToEndCb(self, unused_button):
        end = self.app.project_manager.current_project.pipeline.getDuration()
        self.app.project_manager.current_project.pipeline.simple_seek(end)
        self.app.gui.focusTimeline()
        self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.CENTER, when_not_in_view=True)

    # Public methods for controlling playback

    def undock(self, *unused_widget):
        if not self.docked:
            self.warning("The viewer is already undocked")
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.remove(self.buttons_container)
        position = None
        if self.pipeline:
            position = self.pipeline.getPosition()
            self.pipeline.setState(Gst.State.NULL)
            self.remove(self.target)
            self.__createNewViewer()
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)

        self.undock_button.hide()
        self.fullscreen_button = Gtk.ToggleToolButton()
        self.fullscreen_button.set_icon_name("view-fullscreen")
        self.fullscreen_button.set_tooltip_text(
            _("Show this window in fullscreen"))
        self.buttons.pack_end(
            self.fullscreen_button, expand=False, fill=False, padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggleFullscreen)

        self.external_window.show()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(
            self.settings.viewerWidth, self.settings.viewerHeight)
        if self.pipeline:
            self.pipeline.pause()
            self.pipeline.simple_seek(position)

    def dock(self):
        if self.docked:
            self.warning("The viewer is already docked")
            return

        self.docked = True
        self.settings.viewerDocked = True

        if self.pipeline:
            position = self.pipeline.getPosition()
            self.pipeline.setState(Gst.State.NULL)
            self.external_vbox.remove(self.target)
            self.__createNewViewer()

        self.undock_button.show()
        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()

        self.external_window.hide()
        if position:
            self.pipeline.pause()
            self.pipeline.simple_seek(position)

    def _toggleFullscreen(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """Updates the viewer UI widgets if the timeline position changed.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, clip, position):
        """Shows a live preview of a clip being trimmed."""
        if not hasattr(clip, "get_uri") or isinstance(clip, GES.TitleClip) or clip.props.is_image:
            self.log(
                "%s is an image or has no URI, so not previewing trim" % clip)
            return False

        clip_uri = clip.props.uri
        cur_time = time()
        if self.pipeline == self.app.project_manager.current_project.pipeline:
            self.debug("Creating temporary pipeline for clip %s, position %s",
                       clip_uri, format_ns(position))
            self._oldTimelinePos = self.pipeline.getPosition(False)
            self.pipeline.set_state(Gst.State.NULL)
            self.setPipeline(AssetPipeline(clip))
            self.__owning_pipeline = True
            self._lastClipTrimTime = cur_time

        if (cur_time - self._lastClipTrimTime) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED:
            # Do not seek more than once every 200 ms (for performance)
            self.pipeline.simple_seek(position)
            self._lastClipTrimTime = cur_time

    def clipTrimPreviewFinished(self):
        """Switches back to the project pipeline following a clip trimming."""
        if self.pipeline is not self.app.project_manager.current_project.pipeline:
            self.pipeline.setState(Gst.State.NULL)
            # Using pipeline.getPosition() here does not work because for some
            # reason it's a bit off, that's why we need self._oldTimelinePos.
            self.setPipeline(
                self.app.project_manager.current_project.pipeline, self._oldTimelinePos)
            self._oldTimelinePos = None
            self.debug("Back to the project's pipeline")

    def _pipelineStateChangedCb(self, unused_pipeline, state, old_state):
        """Updates the widgets when the playback starts or stops."""
        if state == Gst.State.PLAYING:
            st = Gst.Structure.new_empty("play")
            self.app.write_action(st)
            self.playpause_button.setPause()
            self.app.simple_inhibit(ViewerContainer.INHIBIT_REASON,
                                    Gtk.ApplicationInhibitFlags.IDLE)
        else:
            if state == Gst.State.PAUSED:
                if old_state != Gst.State.PAUSED:
                    st = Gst.Structure.new_empty("pause")
                    if old_state == Gst.State.PLAYING:
                        st.set_value("playback_time",
                                     self.pipeline.getPosition() / Gst.SECOND)
                    self.app.write_action(st)

                self.playpause_button.setPlay()
            self.app.simple_uninhibit(ViewerContainer.INHIBIT_REASON)
Ejemplo n.º 6
0
class PitiviViewer(Gtk.VBox, Loggable):
    """
    A Widget to control and visualize a Pipeline

    @ivar pipeline: The current pipeline
    @type pipeline: L{Pipeline}
    @ivar action: The action controlled by this Pipeline
    @type action: L{ViewAction}
    """
    __gtype_name__ = 'PitiviViewer'
    __gsignals__ = {
        "activate-playback-controls": (GObject.SignalFlags.RUN_LAST,
            None, (GObject.TYPE_BOOLEAN,)),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app, undock_action=None):
        Gtk.VBox.__init__(self)
        self.set_border_width(SPACING)
        self.app = app
        self.settings = app.settings
        self.system = app.system

        Loggable.__init__(self)
        self.log("New PitiviViewer")

        self.pipeline = None
        self._tmp_pipeline = None  # Used for displaying a preview when trimming

        self.sink = None
        self.docked = True

        # Only used for restoring the pipeline position after a live clip trim preview:
        self._oldTimelinePos = None

        self._haveUI = False

        self._createUi()
        self.target = self.internal
        self.undock_action = undock_action
        if undock_action:
            self.undock_action.connect("activate", self._toggleDocked)

            if not self.settings.viewerDocked:
                self.undock()

    def setPipeline(self, pipeline, position=None):
        """
        Set the Viewer to the given Pipeline.

        Properly switches the currently set action to that new Pipeline.

        @param pipeline: The Pipeline to switch to.
        @type pipeline: L{Pipeline}.
        @param position: Optional position to seek to initially.
        """
        self.debug("self.pipeline:%r", self.pipeline)

        self.seeker = Seeker()
        self._disconnectFromPipeline()
        if self.pipeline:
            self.pipeline.set_state(Gst.State.NULL)

        self.pipeline = pipeline
        if self.pipeline:
            self.pipeline.pause()
            self.seeker.seek(position)

            self.pipeline.connect("state-change", self._pipelineStateChangedCb)
            self.pipeline.connect("position", self._positionCb)
            self.pipeline.connect("duration-changed", self._durationChangedCb)

        self.sink = pipeline.video_overlay
        self._switch_output_window()
        self._setUiActive()

    def _disconnectFromPipeline(self):
        self.debug("pipeline:%r", self.pipeline)
        if self.pipeline is None:
            # silently return, there's nothing to disconnect from
            return

        self.pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        self.pipeline.disconnect_by_func(self._positionCb)
        self.pipeline.disconnect_by_func(self._durationChangedCb)

        self.pipeline = None

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        self.set_sensitive(active)
        if self._haveUI:
            for item in [self.goToStart_button, self.back_button,
                         self.playpause_button, self.forward_button,
                         self.goToEnd_button, self.timecode_entry]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, window, event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _createUi(self):
        """ Creates the Viewer GUI """
        # Drawing area
        # The aspect ratio gets overridden on startup by setDisplayAspectRatio
        self.aframe = Gtk.AspectFrame(xalign=0.5, yalign=1.0, ratio=4.0 / 3.0,
                                      obey_child=False)

        self.internal = ViewerWidget(self.app.settings)
        self.internal.init_transformation_events()
        self.internal.show()
        self.aframe.add(self.internal)
        self.pack_start(self.aframe, True, True, 0)

        self.external_window = Gtk.Window()
        vbox = Gtk.VBox()
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external = ViewerWidget(self.app.settings)
        vbox.pack_start(self.external, True, True, 0)
        self.external_window.connect("delete-event", self._externalWindowDeleteCb)
        self.external_window.connect("configure-event", self._externalWindowConfigureCb)
        self.external_vbox = vbox
        self.external_vbox.show_all()

        # Buttons/Controls
        bbox = Gtk.HBox()
        boxalign = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0.0)
        boxalign.add(bbox)
        self.pack_start(boxalign, False, True, 0)

        self.goToStart_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_PREVIOUS)
        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_tooltip_text(_("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, True, 0)

        self.back_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_REWIND)
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, True, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, True, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_FORWARD)
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, True, 0)

        self.goToEnd_button = Gtk.ToolButton(Gtk.STOCK_MEDIA_NEXT)
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(_("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, True, 0)

        # current time
        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(_('Enter a timecode or frame number\nand press "Enter" to go to that position'))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        self.timecode_entry.connectFocusEvents(self._entryFocusInCb, self._entryFocusOutCb)
        bbox.pack_start(self.timecode_entry, False, 10, 0)
        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")

        screen = Gdk.Screen.get_default()
        height = screen.get_height()
        if height >= 800:
            # show the controls and force the aspect frame to have at least the same
            # width (+110, which is a magic number to minimize dead padding).
            bbox.show_all()
            req = bbox.size_request()
            width = req.width
            height = req.height
            width += 110
            height = int(width / self.aframe.props.ratio)
            self.aframe.set_size_request(width, height)
        self.show_all()
        self.buttons = bbox
        self.buttons_container = boxalign

    def setDisplayAspectRatio(self, ratio):
        """
        Sets the DAR of the Viewer to the given ratio.

        @arg ratio: The aspect ratio to set on the viewer
        @type ratio: L{float}
        """
        self.debug("Setting ratio of %f [%r]", float(ratio), ratio)
        try:
            self.aframe.set_property("ratio", float(ratio))
        except:
            self.warning("could not set ratio !")

    def _entryActivateCb(self, entry):
        self._seekFromTimecodeWidget()

    def _entryFocusInCb(self, entry, event):
        self.app.gui.setActionsSensitive(False)

    def _entryFocusOutCb(self, entry, event):
        self._seekFromTimecodeWidget()
        self.app.gui.setActionsSensitive(True)

    def _seekFromTimecodeWidget(self):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.seeker.seek(nanoseconds)

    ## active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        if duration == 0:
            self._setUiActive(False)
        else:
            self._setUiActive(True)

    ## Control Gtk.Button callbacks

    def setZoom(self, zoom):
        """
        Zoom in or out of the transformation box canvas.
        This is called by clipproperties.
        """
        if self.target.box:
            maxSize = self.target.area
            width = int(float(maxSize.width) * zoom)
            height = int(float(maxSize.height) * zoom)
            area = ((maxSize.width - width) / 2,
                    (maxSize.height - height) / 2,
                    width, height)
            self.sink.set_render_rectangle(*area)
            self.target.box.update_size(area)
            self.target.zoom = zoom
            self.target.sink = self.sink
            self.target.renderbox()

    def _playButtonCb(self, unused_button, playing):
        self.app.current.pipeline.togglePlayback()

    def _goToStartCb(self, unused_button):
        self.seeker.seek(0)

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.seeker.seekRelative(0 - Gst.SECOND)

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.seeker.seekRelative(Gst.SECOND)

    def _goToEndCb(self, unused_button):
        try:
            end = self.app.current.pipeline.getDuration()
        except:
            self.warning("Couldn't get timeline duration")
        try:
            self.seeker.seek(end)
        except:
            self.warning("Couldn't seek to the end of the timeline")

    ## public methods for controlling playback

    def undock(self):
        if not self.undock_action:
            self.error("Cannot undock because undock_action is missing.")
            return
        if not self.docked:
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.undock_action.set_label(_("Dock Viewer"))
        self.target = self.external

        self.remove(self.buttons_container)
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)
        self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
        self.external_window.show()

        self.fullscreen_button = Gtk.ToggleToolButton(Gtk.STOCK_FULLSCREEN)
        self.fullscreen_button.set_tooltip_text(_("Show this window in fullscreen"))
        self.buttons.pack_end(self.fullscreen_button, expand=False, fill=False, padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggleFullscreen)

        # if we are playing, switch output immediately
        if self.sink:
            self._switch_output_window()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(self.settings.viewerWidth, self.settings.viewerHeight)

    def dock(self):
        if not self.undock_action:
            self.error("Cannot dock because undock_action is missing.")
            return
        if self.docked:
            return
        self.docked = True
        self.settings.viewerDocked = True
        self.undock_action.set_label(_("Undock Viewer"))
        self.target = self.internal

        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()
        # if we are playing, switch output immediately
        if self.sink:
            self._switch_output_window()
        self.external_window.hide()

    def _toggleDocked(self, action):
        if self.docked:
            self.undock()
        else:
            self.dock()

    def _toggleFullscreen(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """
        If the timeline position changed, update the viewer UI widgets.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, tl_obj, position):
        """
        While a clip is being trimmed, show a live preview of it.
        """
        if isinstance(tl_obj, GES.TitleClip) or tl_obj.props.is_image or not hasattr(tl_obj, "get_uri"):
            self.log("%s is an image or has no URI, so not previewing trim" % tl_obj)
            return False

        clip_uri = tl_obj.props.uri
        cur_time = time()
        if not self._tmp_pipeline:
            self.debug("Creating temporary pipeline for clip %s, position %s",
                clip_uri, print_ns(position))

            self._oldTimelinePos = self.pipeline.getPosition()
            self._tmp_pipeline = Gst.ElementFactory.make("playbin", None)
            self._tmp_pipeline.set_property("uri", clip_uri)
            self.setPipeline(SimplePipeline(self._tmp_pipeline, self._tmp_pipeline))
            self._lastClipTrimTime = cur_time
        if (cur_time - self._lastClipTrimTime) > 0.2:
            # Do not seek more than once every 200 ms (for performance)
            self._tmp_pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)
            self._lastClipTrimTime = cur_time

    def clipTrimPreviewFinished(self):
        """
        After trimming a clip, reset the project pipeline into the viewer.
        """
        if self._tmp_pipeline is not None:
            self._tmp_pipeline.set_state(Gst.State.NULL)
            self._tmp_pipeline = None  # Free the memory
            self.setPipeline(self.app.current.pipeline, self._oldTimelinePos)
            self.debug("Back to old pipeline")

    def _pipelineStateChangedCb(self, pipeline, state):
        """
        When playback starts/stops, update the viewer widget,
        play/pause button and (un)inhibit the screensaver.

        This is meant to be called by mainwindow.
        """
        self.info("current state changed : %s", state)
        if int(state) == int(Gst.State.PLAYING):
            self.playpause_button.setPause()
            self.system.inhibitScreensaver(self.INHIBIT_REASON)
        elif int(state) == int(Gst.State.PAUSED):
            self.playpause_button.setPlay()
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        else:
            self.sink = None
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        self.internal._currentStateCb(self.pipeline, state)

    def _switch_output_window(self):
        Gdk.threads_enter()
        # Prevent cases where target has no "window_xid" (yes, it happens!):
        self.target.show()
        self.sink.set_window_handle(self.target.window_xid)
        self.sink.expose()
        Gdk.threads_leave()
Ejemplo n.º 7
0
class ViewerContainer(Gtk.VBox, Loggable):
    """
    A wiget holding a viewer and the controls.
    """
    __gtype_name__ = 'ViewerContainer'
    __gsignals__ = {
        "activate-playback-controls": (GObject.SignalFlags.RUN_LAST,
            None, (GObject.TYPE_BOOLEAN,)),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app, undock_action=None):
        Gtk.VBox.__init__(self)
        self.set_border_width(SPACING)
        self.app = app
        self.settings = app.settings
        self.system = app.system

        Loggable.__init__(self)
        self.log("New ViewerContainer")

        self.pipeline = None
        self.docked = True
        self.seeker = Seeker()

        # Only used for restoring the pipeline position after a live clip trim preview:
        self._oldTimelinePos = None

        self._haveUI = False

        self._createUi()
        self.undock_action = undock_action
        if undock_action:
            self.undock_action.connect("activate", self._toggleDocked)

            if not self.settings.viewerDocked:
                self.undock()

    @property
    def target(self):
        if self.docked:
            return self.internal
        else:
            return self.external

    def setPipeline(self, pipeline, position=None):
        """
        Set the Viewer to the given Pipeline.

        Properly switches the currently set action to that new Pipeline.

        @param pipeline: The Pipeline to switch to.
        @type pipeline: L{Pipeline}.
        @param position: Optional position to seek to initially.
        """
        self._disconnectFromPipeline()

        self.debug("New pipeline: %r", pipeline)
        self.pipeline = pipeline
        self.pipeline.pause()
        self.seeker.seek(position)

        self.pipeline.connect("state-change", self._pipelineStateChangedCb)
        self.pipeline.connect("position", self._positionCb)
        self.pipeline.connect("duration-changed", self._durationChangedCb)

        self._switch_output_window()
        self._setUiActive()

    def _disconnectFromPipeline(self):
        self.debug("Previous pipeline: %r", self.pipeline)
        if self.pipeline is None:
            # silently return, there's nothing to disconnect from
            return

        self.pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        self.pipeline.disconnect_by_func(self._positionCb)
        self.pipeline.disconnect_by_func(self._durationChangedCb)

        self.pipeline = None

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        self.set_sensitive(active)
        if self._haveUI:
            for item in [self.goToStart_button, self.back_button,
                         self.playpause_button, self.forward_button,
                         self.goToEnd_button, self.timecode_entry]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, unused_window, unused_event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, unused_window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _videoRealizedCb(self, unused_drawing_area, viewer):
        if viewer == self.target:
            self.log("Viewer widget realized: %s", viewer)
            self._switch_output_window()

    def _createUi(self):
        """ Creates the Viewer GUI """
        # Drawing area
        self.internal = ViewerWidget(self.app.settings, realizedCb=self._videoRealizedCb)
        # Transformation boxed DISABLED
        # self.internal.init_transformation_events()
        self.pack_start(self.internal, True, True, 0)

        self.external_window = Gtk.Window()
        vbox = Gtk.VBox()
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external = ViewerWidget(self.app.settings, realizedCb=self._videoRealizedCb)
        vbox.pack_start(self.external, True, True, 0)
        self.external_window.connect("delete-event", self._externalWindowDeleteCb)
        self.external_window.connect("configure-event", self._externalWindowConfigureCb)
        self.external_vbox = vbox

        # Buttons/Controls
        bbox = Gtk.HBox()
        boxalign = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0.0)
        boxalign.add(bbox)
        self.pack_start(boxalign, False, True, SPACING)

        self.goToStart_button = Gtk.ToolButton()
        self.goToStart_button.set_icon_name("media-skip-backward")
        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_tooltip_text(_("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, True, 0)

        self.back_button = Gtk.ToolButton()
        self.back_button.set_icon_name("media-seek-backward")
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, True, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, True, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.ToolButton()
        self.forward_button.set_icon_name("media-seek-forward")
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, True, 0)

        self.goToEnd_button = Gtk.ToolButton()
        self.goToEnd_button.set_icon_name("media-skip-forward")
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(_("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, True, 0)

        # current time
        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(_('Enter a timecode or frame number\nand press "Enter" to go to that position'))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        bbox.pack_start(self.timecode_entry, False, 10, 0)
        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")

        screen = Gdk.Screen.get_default()
        height = screen.get_height()
        if height >= 800:
            # show the controls and force the aspect frame to have at least the same
            # width (+110, which is a magic number to minimize dead padding).
            bbox.show_all()
            req = bbox.size_request()
            width = req.width
            height = req.height
            width += 110
            height = int(width / self.internal.props.ratio)
            self.internal.set_size_request(width, height)

        self.buttons = bbox
        self.buttons_container = boxalign
        self.show_all()
        self.external_vbox.show_all()

    def setDisplayAspectRatio(self, ratio):
        self.debug("Setting aspect ratio to %f [%r]", float(ratio), ratio)
        self.internal.setDisplayAspectRatio(ratio)
        self.external.setDisplayAspectRatio(ratio)

    def _entryActivateCb(self, unused_entry):
        self._seekFromTimecodeWidget()

    def _seekFromTimecodeWidget(self):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.seeker.seek(nanoseconds)

    ## active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        if duration == 0:
            self._setUiActive(False)
        else:
            self._setUiActive(True)

    ## Control Gtk.Button callbacks

    def setZoom(self, zoom):
        """
        Zoom in or out of the transformation box canvas.
        This is called by clipproperties.
        """
        if self.target.box:
            maxSize = self.target.area
            width = int(float(maxSize.width) * zoom)
            height = int(float(maxSize.height) * zoom)
            area = ((maxSize.width - width) / 2,
                    (maxSize.height - height) / 2,
                    width, height)
            self.sink.set_render_rectangle(*area)
            self.target.box.update_size(area)
            self.target.zoom = zoom
            self.target.renderbox()

    def _playButtonCb(self, unused_button, unused_playing):
        self.app.current_project.pipeline.togglePlayback()
        self.app.gui.focusTimeline()

    def _goToStartCb(self, unused_button):
        self.seeker.seek(0)
        self.app.gui.focusTimeline()

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.seeker.seekRelative(0 - Gst.SECOND)
        self.app.gui.focusTimeline()

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.seeker.seekRelative(Gst.SECOND)
        self.app.gui.focusTimeline()

    def _goToEndCb(self, unused_button):
        end = self.app.current_project.pipeline.getDuration()
        self.seeker.seek(end)
        self.app.gui.focusTimeline()

    ## public methods for controlling playback

    def undock(self):
        if not self.undock_action:
            self.error("Cannot undock because undock_action is missing.")
            return
        if not self.docked:
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.undock_action.set_label(_("Dock Viewer"))

        self.remove(self.buttons_container)
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)
        self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
        self.external_window.show()

        self.fullscreen_button = Gtk.ToggleToolButton()
        self.fullscreen_button.set_icon_name("view-fullscreen")
        self.fullscreen_button.set_tooltip_text(_("Show this window in fullscreen"))
        self.buttons.pack_end(self.fullscreen_button, expand=False, fill=False, padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggleFullscreen)

        # if we are playing, switch output immediately
        if self.pipeline:
            self._switch_output_window()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(self.settings.viewerWidth, self.settings.viewerHeight)

    def dock(self):
        if not self.undock_action:
            self.error("Cannot dock because undock_action is missing.")
            return
        if self.docked:
            return
        self.docked = True
        self.settings.viewerDocked = True
        self.undock_action.set_label(_("Undock Viewer"))

        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()
        # if we are playing, switch output immediately
        if self.pipeline:
            self._switch_output_window()
        self.external_window.hide()

    def _toggleDocked(self, unused_action):
        if self.docked:
            self.undock()
        else:
            self.dock()

    def _toggleFullscreen(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """
        If the timeline position changed, update the viewer UI widgets.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, tl_obj, position):
        """
        While a clip is being trimmed, show a live preview of it.
        """
        if isinstance(tl_obj, GES.TitleClip) or tl_obj.props.is_image or not hasattr(tl_obj, "get_uri"):
            self.log("%s is an image or has no URI, so not previewing trim" % tl_obj)
            return False

        clip_uri = tl_obj.props.uri
        cur_time = time()
        if self.pipeline == self.app.current_project.pipeline:
            self.debug("Creating temporary pipeline for clip %s, position %s",
                clip_uri, format_ns(position))
            self._oldTimelinePos = self.pipeline.getPosition()
            self.setPipeline(AssetPipeline(tl_obj))
            self._lastClipTrimTime = cur_time

        if (cur_time - self._lastClipTrimTime) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED:
            # Do not seek more than once every 200 ms (for performance)
            self.pipeline.simple_seek(position)
            self._lastClipTrimTime = cur_time

    def clipTrimPreviewFinished(self):
        """
        After trimming a clip, reset the project pipeline into the viewer.
        """
        if self.pipeline is not self.app.current_project.pipeline:
            self.pipeline.setState(Gst.State.NULL)
            # Using pipeline.getPosition() here does not work because for some
            # reason it's a bit off, that's why we need self._oldTimelinePos.
            self.setPipeline(self.app.current_project.pipeline, self._oldTimelinePos)
            self.debug("Back to the project's pipeline")

    def _pipelineStateChangedCb(self, unused_pipeline, state):
        """
        When playback starts/stops, update the viewer widget,
        play/pause button and (un)inhibit the screensaver.

        This is meant to be called by mainwindow.
        """
        if int(state) == int(Gst.State.PLAYING):
            self.playpause_button.setPause()
            self.system.inhibitScreensaver(self.INHIBIT_REASON)
        elif int(state) == int(Gst.State.PAUSED):
            self.playpause_button.setPlay()
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        else:
            self.system.uninhibitScreensaver(self.INHIBIT_REASON)
        self.internal._currentStateCb(self.pipeline, state)

    def _switch_output_window(self):
        # Don't do anything if we don't have a pipeline
        if self.pipeline is None:
            return

        if self.target.get_realized():
            self.debug("Connecting the pipeline to the viewer's texture")
            self.pipeline.connectWithViewer(self.target)
        else:
            # Show the widget and wait for the realized callback
            self.log("Target is not realized, showing the widget")
            self.target.show()
Ejemplo n.º 8
0
class ViewerContainer(Gtk.Box, Loggable):
    """Wiget holding a viewer and the controls.

    Attributes:
        pipeline (SimplePipeline): The displayed pipeline.
    """

    __gtype_name__ = 'ViewerContainer'
    __gsignals__ = {
        "activate-playback-controls": (GObject.SignalFlags.RUN_LAST,
                                       None, (GObject.TYPE_BOOLEAN,)),
    }

    INHIBIT_REASON = _("Currently playing")

    def __init__(self, app):
        Gtk.Box.__init__(self)
        self.app = app
        self.settings = app.settings

        Loggable.__init__(self)
        self.log("New ViewerContainer")

        self.project = None
        self.trim_pipeline = None
        self.trim_pipelines_cache = collections.OrderedDict()
        self.docked = True
        self.target = None
        self._compactMode = False

        self._haveUI = False

        self._createUi()

        if not self.settings.viewerDocked:
            self.undock()

        self.__cursor = None
        self.__translation = None

        pm = self.app.project_manager
        pm.connect("new-project-loaded", self._project_manager_new_project_loaded_cb)
        pm.connect("project-closed", self._projectManagerProjectClosedCb)

    def _project_manager_new_project_loaded_cb(self, unused_project_manager, project):
        project.connect("rendering-settings-changed",
                        self._project_rendering_settings_changed_cb)
        self.set_project(project)

    def _projectManagerProjectClosedCb(self, unused_project_manager, project):
        if self.project == project:
            project.disconnect_by_func(self._project_rendering_settings_changed_cb)
        self.project = None

    def _project_rendering_settings_changed_cb(self, project, unused_item):
        """Handles Project metadata changes."""
        self._reset_viewer_aspect_ratio(project)

    def _reset_viewer_aspect_ratio(self, project):
        """Resets the viewer aspect ratio."""
        self.target.update_aspect_ratio(project)
        self.timecode_entry.setFramerate(project.videorate)

    def set_project(self, project):
        """Sets the displayed project.

        Args:
            project (Project): The Project to switch to.
        """
        self.debug("Setting project: %r", project)
        self._disconnectFromPipeline()

        if self.target:
            parent = self.target.get_parent()
            if parent:
                parent.remove(self.target)

        project.pipeline.connect("state-change", self._pipelineStateChangedCb)
        project.pipeline.connect("position", self._positionCb)
        project.pipeline.connect("duration-changed", self._durationChangedCb)
        self.project = project

        self.__createNewViewer()
        self._setUiActive()

        # This must be done at the end, otherwise the created sink widget
        # appears in a separate window.
        project.pipeline.pause()

    def __createNewViewer(self):
        _, sink_widget = self.project.pipeline.create_sink()

        self.overlay_stack = OverlayStack(self.app, sink_widget)
        self.target = ViewerWidget(self.overlay_stack)
        self._reset_viewer_aspect_ratio(self.project)

        if self.docked:
            self.pack_start(self.target, expand=True, fill=True, padding=0)
        else:
            self.external_vbox.pack_start(self.target, expand=True, fill=False, padding=0)
            self.external_vbox.child_set(self.target, fill=True)

        self.target.show_all()

        # Wait for 1s to make sure that the viewer has completely realized
        # and then we can mark the resize status as showable.
        GLib.timeout_add(1000, self.__viewer_realization_done_cb, None)

    def _disconnectFromPipeline(self):
        if self.project is None:
            return

        pipeline = self.project.pipeline
        self.debug("Disconnecting from: %r", pipeline)
        pipeline.disconnect_by_func(self._pipelineStateChangedCb)
        pipeline.disconnect_by_func(self._positionCb)
        pipeline.disconnect_by_func(self._durationChangedCb)

    def _setUiActive(self, active=True):
        self.debug("active %r", active)
        if self._haveUI:
            for item in [self.goToStart_button, self.back_button,
                         self.playpause_button, self.forward_button,
                         self.goToEnd_button, self.timecode_entry]:
                item.set_sensitive(active)
        if active:
            self.emit("activate-playback-controls", True)

    def _externalWindowDeleteCb(self, unused_window, unused_event):
        self.dock()
        return True

    def _externalWindowConfigureCb(self, unused_window, event):
        self.settings.viewerWidth = event.width
        self.settings.viewerHeight = event.height
        self.settings.viewerX = event.x
        self.settings.viewerY = event.y

    def _createUi(self):
        """Creates the Viewer GUI."""
        self.set_orientation(Gtk.Orientation.VERTICAL)

        self.external_window = Gtk.Window()
        vbox = Gtk.Box()
        vbox.set_orientation(Gtk.Orientation.VERTICAL)
        vbox.set_spacing(SPACING)
        self.external_window.add(vbox)
        self.external_window.connect(
            "delete-event", self._externalWindowDeleteCb)
        self.external_window.connect(
            "configure-event", self._externalWindowConfigureCb)
        self.external_vbox = vbox

        # Corner marker.
        corner = Gtk.DrawingArea()
        # Number of lines to draw in the corner marker.
        lines = 3
        # Space between each line.
        space = 5
        # Margin from left and bottom of viewer container.
        margin = 2
        corner_size = space * lines + margin
        corner.set_size_request(corner_size, corner_size)
        corner.set_halign(Gtk.Align.START)
        corner.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK |
                          Gdk.EventMask.BUTTON_PRESS_MASK |
                          Gdk.EventMask.BUTTON_RELEASE_MASK |
                          Gdk.EventMask.POINTER_MOTION_MASK)
        hpane = self.app.gui.editor.mainhpaned
        vpane = self.app.gui.editor.toplevel_widget
        corner.connect("draw", self.__corner_draw_cb, lines, space, margin)
        corner.connect("enter-notify-event", self.__corner_enter_notify_cb)
        corner.connect("button-press-event", self.__corner_button_press_cb, hpane, vpane)
        corner.connect("button-release-event", self.__corner_button_release_cb)
        corner.connect("motion-notify-event", self.__corner_motion_notify_cb, hpane, vpane)
        self.pack_end(corner, False, False, 0)

        # Buttons/Controls
        bbox = Gtk.Box()
        bbox.set_orientation(Gtk.Orientation.HORIZONTAL)
        bbox.set_property("valign", Gtk.Align.CENTER)
        bbox.set_property("halign", Gtk.Align.CENTER)
        bbox.set_margin_left(SPACING)
        bbox.set_margin_right(SPACING)
        self.pack_end(bbox, False, False, 0)

        self.goToStart_button = Gtk.Button.new_from_icon_name("media-skip-backward-symbolic",
                                                              Gtk.IconSize.BUTTON)

        self.goToStart_button.connect("clicked", self._goToStartCb)
        self.goToStart_button.set_relief(Gtk.ReliefStyle.NONE)
        self.goToStart_button.set_tooltip_text(
            _("Go to the beginning of the timeline"))
        self.goToStart_button.set_sensitive(False)
        bbox.pack_start(self.goToStart_button, False, False, 0)

        self.back_button = Gtk.Button.new_from_icon_name("media-seek-backward-symbolic",
                                                         Gtk.IconSize.BUTTON)

        self.back_button.set_relief(Gtk.ReliefStyle.NONE)
        self.back_button.connect("clicked", self._backCb)
        self.back_button.set_tooltip_text(_("Go back one second"))
        self.back_button.set_sensitive(False)
        bbox.pack_start(self.back_button, False, False, 0)

        self.playpause_button = PlayPauseButton()
        self.playpause_button.connect("play", self._playButtonCb)
        bbox.pack_start(self.playpause_button, False, False, 0)
        self.playpause_button.set_sensitive(False)

        self.forward_button = Gtk.Button.new_from_icon_name("media-seek-forward-symbolic",
                                                            Gtk.IconSize.BUTTON)
        self.forward_button.set_relief(Gtk.ReliefStyle.NONE)
        self.forward_button.connect("clicked", self._forwardCb)
        self.forward_button.set_tooltip_text(_("Go forward one second"))
        self.forward_button.set_sensitive(False)
        bbox.pack_start(self.forward_button, False, False, 0)

        self.goToEnd_button = Gtk.Button.new_from_icon_name("media-skip-forward-symbolic",
                                                            Gtk.IconSize.BUTTON)
        self.goToEnd_button.set_relief(Gtk.ReliefStyle.NONE)
        self.goToEnd_button.connect("clicked", self._goToEndCb)
        self.goToEnd_button.set_tooltip_text(
            _("Go to the end of the timeline"))
        self.goToEnd_button.set_sensitive(False)
        bbox.pack_start(self.goToEnd_button, False, False, 0)

        self.timecode_entry = TimeWidget()
        self.timecode_entry.setWidgetValue(0)
        self.timecode_entry.set_tooltip_text(
            _('Enter a timecode or frame number\nand press "Enter" to go to that position'))
        self.timecode_entry.connectActivateEvent(self._entryActivateCb)
        self.timecode_entry.connect("key_press_event", self._entry_key_press_event_cb)
        bbox.pack_start(self.timecode_entry, False, False, 15)

        self.undock_button = Gtk.Button.new_from_icon_name("view-restore-symbolic",
                                                           Gtk.IconSize.BUTTON)

        self.undock_button.set_relief(Gtk.ReliefStyle.NONE)
        self.undock_button.connect("clicked", self.undock_cb)
        self.undock_button.set_tooltip_text(
            _("Detach the viewer\nYou can re-attach it by closing the newly created window."))
        bbox.pack_start(self.undock_button, False, False, 0)

        self.show_all()

        # Create a hidden container for the clip trim preview video widget.
        self.hidden_chest = Gtk.Frame()
        # It has to be added to the window, otherwise when we add
        # a video widget to it, it will create a new window!
        self.pack_end(self.hidden_chest, False, False, 0)

        self._haveUI = True

        # Identify widgets for AT-SPI, making our test suite easier to develop
        # These will show up in sniff, accerciser, etc.
        self.goToStart_button.get_accessible().set_name("goToStart_button")
        self.back_button.get_accessible().set_name("back_button")
        self.playpause_button.get_accessible().set_name("playpause_button")
        self.forward_button.get_accessible().set_name("forward_button")
        self.goToEnd_button.get_accessible().set_name("goToEnd_button")
        self.timecode_entry.get_accessible().set_name("timecode_entry")
        self.undock_button.get_accessible().set_name("undock_button")

        self.buttons_container = bbox
        self.external_vbox.show_all()

    def __corner_draw_cb(self, unused_widget, cr, lines, space, margin):
        cr.set_line_width(1)

        marker_color = self.app.gui.get_style_context().lookup_color("borders")
        cr.set_source_rgb(marker_color.color.red,
                          marker_color.color.green,
                          marker_color.color.blue)

        cr.translate(margin, 0)
        for i in range(lines):
            cr.move_to(0, space * i)
            cr.line_to(space * (lines - i), space * lines)
            cr.stroke()

    def __corner_enter_notify_cb(self, widget, unused_event):
        if not self.__cursor:
            self.__cursor = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_LEFT_CORNER)
        widget.get_window().set_cursor(self.__cursor)

    def __corner_button_press_cb(self, unused_widget, event, hpane, vpane):
        if event.button == 1:
            # The mouse pointer position is w.r.t the root of the screen
            # whereas the positions of panes is w.r.t the root of the
            # mainwindow. We need to find the translation that takes us
            # from screen coordinate system to mainwindow coordinate system.
            self.__translation = (event.x_root - hpane.get_position(),
                                  event.y_root - vpane.get_position())

    def __corner_button_release_cb(self, unused_widget, unused_event):
        self.__translation = None

    def __corner_motion_notify_cb(self, unused_widget, event, hpane, vpane):
        if self.__translation is None:
            return

        hpane.set_position(event.x_root - self.__translation[0])
        vpane.set_position(event.y_root - self.__translation[1])

    def activateCompactMode(self):
        self.back_button.hide()
        self.forward_button.hide()
        self._compactMode = True  # Prevent set_size_request later

    def _entryActivateCb(self, unused_entry):
        nanoseconds = self.timecode_entry.getWidgetValue()
        self.app.project_manager.current_project.pipeline.simple_seek(nanoseconds)
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.CENTER, when_not_in_view=True)

    def _entry_key_press_event_cb(self, widget, event):
        """Handles the key press events in the timecode_entry widget."""
        if event.keyval == Gdk.KEY_Escape:
            self.app.gui.editor.focusTimeline()

    # Active Timeline calllbacks
    def _durationChangedCb(self, unused_pipeline, duration):
        self._setUiActive(duration > 0)

    def _playButtonCb(self, unused_button, unused_playing):
        self.app.project_manager.current_project.pipeline.togglePlayback()
        self.app.gui.editor.focusTimeline()

    def _goToStartCb(self, unused_button):
        self.app.project_manager.current_project.pipeline.simple_seek(0)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.START, when_not_in_view=True)

    def _backCb(self, unused_button):
        # Seek backwards one second
        self.app.project_manager.current_project.pipeline.seekRelative(0 - Gst.SECOND)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.END, when_not_in_view=True)

    def _forwardCb(self, unused_button):
        # Seek forward one second
        self.app.project_manager.current_project.pipeline.seekRelative(Gst.SECOND)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.START, when_not_in_view=True)

    def _goToEndCb(self, unused_button):
        end = self.app.project_manager.current_project.pipeline.getDuration()
        self.app.project_manager.current_project.pipeline.simple_seek(end)
        self.app.gui.editor.focusTimeline()
        self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead(
            align=Gtk.Align.CENTER, when_not_in_view=True)

    def undock_cb(self, unused_widget):
        self.undock()

    # Public methods for controlling playback

    def undock(self):
        if not self.docked:
            self.warning("The viewer is already undocked")
            return

        self.docked = False
        self.settings.viewerDocked = False
        self.remove(self.buttons_container)
        position = None
        if self.project:
            self.overlay_stack.enable_resize_status(False)
            position = self.project.pipeline.getPosition()
            self.project.pipeline.setState(Gst.State.NULL)
            self.remove(self.target)
            self.__createNewViewer()
        self.buttons_container.set_margin_bottom(SPACING)
        self.external_vbox.pack_end(self.buttons_container, False, False, 0)

        self.undock_button.hide()
        self.fullscreen_button = Gtk.ToggleButton()
        fullscreen_image = Gtk.Image.new_from_icon_name(
            "view-fullscreen-symbolic", Gtk.IconSize.BUTTON)
        self.fullscreen_button.set_image(fullscreen_image)
        self.fullscreen_button.set_tooltip_text(
            _("Show this window in fullscreen"))
        self.fullscreen_button.set_relief(Gtk.ReliefStyle.NONE)
        self.buttons_container.pack_end(
            self.fullscreen_button, expand=False, fill=False, padding=6)
        self.fullscreen_button.show()
        self.fullscreen_button.connect("toggled", self._toggle_fullscreen_cb)

        self.external_window.show()
        self.hide()
        self.external_window.move(self.settings.viewerX, self.settings.viewerY)
        self.external_window.resize(
            self.settings.viewerWidth, self.settings.viewerHeight)
        if self.project:
            self.project.pipeline.pause()
            self.project.pipeline.simple_seek(position)

    def __viewer_realization_done_cb(self, unused_data):
        self.overlay_stack.enable_resize_status(True)
        return False

    def dock(self):
        if self.docked:
            self.warning("The viewer is already docked")
            return

        self.docked = True
        self.settings.viewerDocked = True

        position = None
        if self.project:
            self.overlay_stack.enable_resize_status(False)
            position = self.project.pipeline.getPosition()
            self.project.pipeline.setState(Gst.State.NULL)
            self.external_vbox.remove(self.target)
            self.__createNewViewer()

        self.undock_button.show()
        self.fullscreen_button.destroy()
        self.external_vbox.remove(self.buttons_container)
        self.buttons_container.set_margin_bottom(0)
        self.pack_end(self.buttons_container, False, False, 0)
        self.show()

        self.external_window.hide()
        if self.project.pipeline:
            self.project.pipeline.pause()
            self.project.pipeline.simple_seek(position)

    def _toggle_fullscreen_cb(self, widget):
        if widget.get_active():
            self.external_window.hide()
            # GTK doesn't let us fullscreen utility windows
            self.external_window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
            self.external_window.show()
            self.external_window.fullscreen()
            widget.set_tooltip_text(_("Exit fullscreen mode"))
        else:
            self.external_window.unfullscreen()
            widget.set_tooltip_text(_("Show this window in fullscreen"))
            self.external_window.hide()
            self.external_window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
            self.external_window.show()

    def _positionCb(self, unused_pipeline, position):
        """Updates the viewer UI widgets if the timeline position changed.

        This is meant to be called either by the gobject timer when playing,
        or by mainwindow's _timelineSeekCb when the timer is disabled.
        """
        self.timecode_entry.setWidgetValue(position, False)

    def clipTrimPreview(self, clip, position):
        """Shows a live preview of a clip being trimmed."""
        if not hasattr(clip, "get_uri") or isinstance(clip, GES.TitleClip) or clip.props.is_image:
            self.log("Not previewing trim for image or title clip: %s", clip)
            return False

        if self.project.pipeline.getState() == Gst.State.PLAYING:
            self.project.pipeline.setState(Gst.State.PAUSED)

        uri = clip.props.uri
        if self.trim_pipeline and uri != self.trim_pipeline.uri:
            # Seems to be the trim preview pipeline for a different clip.
            self.trim_pipeline.release()
            self.trim_pipeline = None

        if not self.trim_pipeline:
            self.trim_pipeline, sink_widget = self.get_trim_preview_pipeline(uri)
            # Add the widget to a hidden container and make it appear later
            # when it's ready. If we show it before the initial seek completion,
            # there is a flicker when the first frame of the asset is shown for
            # a brief moment until the initial seek to the frame we actually
            # want to show is performed.
            # First make sure the container itself is ready.
            widget = self.hidden_chest.get_child()
            if widget:
                self.warning("The previous trim preview video widget should have been removed already")
                self.hidden_chest.remove(widget)
            self.hidden_chest.add(sink_widget)
            sink_widget.show()
            self.trim_pipeline.connect("state-change", self._state_change_cb)
            self.trim_pipeline.setState(Gst.State.PAUSED)
            self._last_trim_ns = 0

        self.trim_pipeline.simple_seek(position)

    def get_trim_preview_pipeline(self, uri):
        try:
            trim_pipeline, sink_widget = self.trim_pipelines_cache[uri]
            self.debug("Reusing temporary pipeline for clip %s", uri)
        except KeyError:
            self.debug("Creating temporary pipeline for clip %s", uri)
            trim_pipeline = AssetPipeline(uri)
            unused_video_sink, sink_widget = trim_pipeline.create_sink()
        self.trim_pipelines_cache[uri] = trim_pipeline, sink_widget
        if len(self.trim_pipelines_cache) > 4:
            # Pop the first inserted item.
            expired_uri, (expired_pipeline, unused_expired_widget) = self.trim_pipelines_cache.popitem(last=False)
            self.debug("Releasing temporary pipeline for clip %s", expired_uri)
            expired_pipeline.release()
        return trim_pipeline, sink_widget

    def _state_change_cb(self, trim_pipeline, state, prev_state):
        if self.trim_pipeline is not trim_pipeline:
            self.warning("State change reported for previous trim preview pipeline")
            trim_pipeline.disconnect_by_func(self._state_change_cb)
            return
        # First the pipeline goes from READY to PAUSED, and then it goes
        # from PAUSED to PAUSED, and this is a good moment.
        if prev_state == Gst.State.PAUSED and state == Gst.State.PAUSED:
            sink_widget = self.hidden_chest.get_child()
            if sink_widget:
                self.hidden_chest.remove(sink_widget)
                self.target.switch_widget(sink_widget)
            trim_pipeline.disconnect_by_func(self._state_change_cb)

    def clipTrimPreviewFinished(self):
        """Switches back to the project pipeline following a clip trimming."""
        if not self.trim_pipeline:
            return
        self.target.switch_widget(self.overlay_stack)
        self.trim_pipeline = None

    def _pipelineStateChangedCb(self, pipeline, state, old_state):
        """Updates the widgets when the playback starts or stops."""
        if state == Gst.State.PLAYING:
            st = Gst.Structure.new_empty("play")
            self.app.write_action(st)
            self.playpause_button.setPause()
            self.app.simple_inhibit(ViewerContainer.INHIBIT_REASON,
                                    Gtk.ApplicationInhibitFlags.IDLE)
            self.overlay_stack.hide_overlays()
        else:
            if state == Gst.State.PAUSED:
                if old_state != Gst.State.PAUSED:
                    st = Gst.Structure.new_empty("pause")
                    if old_state == Gst.State.PLAYING:
                        position_seconds = pipeline.getPosition() / Gst.SECOND
                        st.set_value("playback_time", position_seconds)
                    self.app.write_action(st)

                self.playpause_button.setPlay()
            self.overlay_stack.show_overlays()
            self.app.simple_uninhibit(ViewerContainer.INHIBIT_REASON)