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()
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()
class ViewerContainer(Gtk.Box, 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.Box.__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() self.target = None # 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): """ 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() 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.seeker.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.sink = self.pipeline.createSink() self.pipeline.setSink(self.sink) self.target = ViewerWidget(self.sink, self.app) if self.docked: self.pack_start(self.target, True, True, 0) 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). req = self.buttons.size_request() width = req.width height = req.height width += 110 height = int(width / self.target.props.ratio) 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) 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 setDisplayAspectRatio(self, ratio): self.debug("Setting aspect ratio to %f [%r]", float(ratio), ratio) self.target.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) 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) 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.seeker.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.seeker.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): """ 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, clip, position): """ While a clip is being trimmed, show a live preview of it. """ 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(True) 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): """ 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._oldTimelinePos = None self.debug("Back to the project's pipeline") def _pipelineStateChangedCb(self, unused_pipeline, state, old_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): st = Gst.Structure.new_empty("play") self.app.write_action(st) self.playpause_button.setPause() self.system.inhibitScreensaver(self.INHIBIT_REASON) elif int(state) == int(Gst.State.PAUSED): if old_state != int(Gst.State.PAUSED): st = Gst.Structure.new_empty("pause") if old_state == int(Gst.State.PLAYING): st.set_value("playback_time", float(self.pipeline.getPosition()) / Gst.SECOND) self.app.write_action(st) self.playpause_button.setPlay() self.system.uninhibitScreensaver(self.INHIBIT_REASON) else: self.system.uninhibitScreensaver(self.INHIBIT_REASON)
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()
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()