def _createUi(self): self.embed = GtkClutter.Embed() self.embed.get_accessible().set_name("timeline canvas") # for dogtail self.stage = self.embed.get_stage() self.timeline = TimelineStage(self) self.controls = ControlContainer(self.timeline) self.zoomBox = ZoomBox(self) self.shiftMask = False self.controlMask = False # TODO: make the bg a gradient from (0, 0, 0, 255) to (50, 50, 50, 255) self.stage.set_background_color(Clutter.Color.new(31, 30, 33, 255)) self.timeline.set_position(CONTROL_WIDTH, 0) self.controls.set_position(0, 0) self.controls.set_z_position(2) self.stage.add_child(self.controls) self.stage.add_child(self.timeline) self.stage.connect("destroy", quit_) self.stage.connect("button-press-event", self._clickedCb) self.stage.connect("button-release-event", self._releasedCb) self.embed.connect("scroll-event", self._scrollEventCb) if self.gui: self.gui.connect("key-press-event", self._keyPressEventCb) self.gui.connect("key-release-event", self._keyReleaseEventCb) self.embed.connect("enter-notify-event", self._enterNotifyEventCb) self.point = Clutter.Point() self.point.x = 0 self.point.y = 0 self.scrolled = 0 self.zoomed_fitted = True self.pressed = False self._packScrollbars(self) self.stage.show()
class Timeline(Gtk.VBox, Zoomable): """ This is the main timeline widget, which will contain the timeline stage and the layer controls, the scrollbars and the ruler. """ def __init__(self, gui, instance, ui_manager): Zoomable.__init__(self) Gtk.VBox.__init__(self) GObject.threads_init() self.gui = gui self.ui_manager = ui_manager self.app = instance self._settings = None if self.app: self._settings = self.app.settings self._projectmanager = None self._project = None self.pipeline = None self._createUi() self._createActions() self._setUpDragAndDrop() if self._settings: self._settings.connect("edgeSnapDeadbandChanged", self._snapDistanceChangedCb) # Standalone if not self._settings: gtksettings = Gtk.Settings.get_default() gtksettings.set_property("gtk-application-prefer-dark-theme", True) self.show_all() # Public API def insertEnd(self, assets): """ Allows to add any asset at the end of the current timeline. """ self.app.action_log.begin("add clip") # FIXME we should find the longest layer instead of adding it to the # first one # Handle the case of a blank project layer = self._ensureLayer()[0] for asset in assets: if isinstance(asset, GES.TitleClip): clip_duration = asset.get_duration() elif asset.is_image(): clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000) else: clip_duration = asset.get_duration() if not isinstance(asset, GES.TitleClip): layer.add_asset(asset, self.bTimeline.props.duration, 0, clip_duration, asset.get_supported_formats()) else: asset.set_start(self.bTimeline.props.duration) layer.add_clip(asset) if self.zoomed_fitted: self._setBestZoomRatio() else: self.scrollToPosition(self.bTimeline.props.duration) def setProjectManager(self, projectmanager): if self._projectmanager is not None: self._projectmanager.disconnect_by_func(self._projectChangedCb) self._projectmanager = projectmanager if projectmanager is not None: projectmanager.connect("new-project-created", self._projectCreatedCb) projectmanager.connect("new-project-loaded", self._projectChangedCb) def updateHScrollAdjustments(self): """ Recalculate the horizontal scrollbar depending on the timeline duration. """ timeline_ui_width = self.embed.get_allocation().width controls_width = 0 scrollbar_width = 0 contents_size = Zoomable.nsToPixel(self.bTimeline.props.duration) widgets_width = controls_width + scrollbar_width end_padding = CONTROL_WIDTH + 250 # Provide some space for clip insertion at the end self.hadj.props.lower = 0 self.hadj.props.upper = contents_size + widgets_width + end_padding self.hadj.props.page_size = timeline_ui_width self.hadj.props.page_increment = contents_size * 0.9 self.hadj.props.step_increment = contents_size * 0.1 if contents_size + widgets_width <= timeline_ui_width: # We're zoomed out completely, re-enable automatic zoom fitting # when adding new clips. self.zoomed_fitted = True def zoomFit(self): self._hscrollBar.set_value(0) self._setBestZoomRatio() def scrollToPosition(self, position): if position > self.hadj.props.upper: # we can't perform the scroll because the canvas needs to be # updated GLib.idle_add(self._scrollToPosition, position) else: self._scrollToPosition(position) def setTimeline(self, bTimeline): self.bTimeline = bTimeline self.timeline.selection.connect("selection-changed", self._selectionChangedCb) self.timeline.setTimeline(bTimeline) def getEditionMode(self, isAHandle=False): if self.shiftMask or (self.gui and self.gui._autoripple_active): return GES.EditMode.EDIT_RIPPLE if isAHandle and self.controlMask: return GES.EditMode.EDIT_ROLL elif isAHandle: return GES.EditMode.EDIT_TRIM return GES.EditMode.EDIT_NORMAL # Internal API def _createUi(self): self.embed = GtkClutter.Embed() self.embed.get_accessible().set_name("timeline canvas") # for dogtail self.stage = self.embed.get_stage() self.timeline = TimelineStage(self) self.controls = ControlContainer(self.timeline) self.zoomBox = ZoomBox(self) self.shiftMask = False self.controlMask = False # TODO: make the bg a gradient from (0, 0, 0, 255) to (50, 50, 50, 255) self.stage.set_background_color(Clutter.Color.new(31, 30, 33, 255)) self.timeline.set_position(CONTROL_WIDTH, 0) self.controls.set_position(0, 0) self.controls.set_z_position(2) self.stage.add_child(self.controls) self.stage.add_child(self.timeline) self.stage.connect("destroy", quit_) self.stage.connect("button-press-event", self._clickedCb) self.stage.connect("button-release-event", self._releasedCb) self.embed.connect("scroll-event", self._scrollEventCb) if self.gui: self.gui.connect("key-press-event", self._keyPressEventCb) self.gui.connect("key-release-event", self._keyReleaseEventCb) self.embed.connect("enter-notify-event", self._enterNotifyEventCb) self.point = Clutter.Point() self.point.x = 0 self.point.y = 0 self.scrolled = 0 self.zoomed_fitted = True self.pressed = False self._packScrollbars(self) self.stage.show() def _setUpDragAndDrop(self): self.dropHighlight = False self.dropOccured = False self.dropDataReady = False self.dropData = None dnd_list = [Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags.OTHER_APP, TARGET_TYPE_URI_LIST)] self.drag_dest_set(0, dnd_list, Gdk.DragAction.COPY) self.drag_dest_add_uri_targets() self.connect('drag-motion', self._dragMotionCb) self.connect('drag-data-received', self._dragDataReceivedCb) self.connect('drag-drop', self._dragDropCb) self.connect('drag-leave', self._dragLeaveCb) def _ensureLayer(self): """ Make sure we have a layer in our timeline Returns: The number of layer present in self.timeline """ layers = self.bTimeline.get_layers() if not layers: layer = GES.Layer() layer.props.auto_transition = True self.bTimeline.add_layer(layer) layers = [layer] return layers def _createActions(self): if not self.gui: return actions = ( ("ZoomIn", Gtk.STOCK_ZOOM_IN, None, "<Control>plus", ZOOM_IN, self._zoomInCb), ("ZoomOut", Gtk.STOCK_ZOOM_OUT, None, "<Control>minus", ZOOM_OUT, self._zoomOutCb), ("ZoomFit", Gtk.STOCK_ZOOM_FIT, None, "<Control>0", ZOOM_FIT, self._zoomFitCb), ("Screenshot", None, _("Export current frame..."), None, _("Export the frame at the current playhead " "position as an image file."), self._screenshotCb), # Alternate keyboard shortcuts to the actions above ("ControlEqualAccel", Gtk.STOCK_ZOOM_IN, None, "<Control>equal", ZOOM_IN, self._zoomInCb), ("ControlKPAddAccel", Gtk.STOCK_ZOOM_IN, None, "<Control>KP_Add", ZOOM_IN, self._zoomInCb), ("ControlKPSubtractAccel", Gtk.STOCK_ZOOM_OUT, None, "<Control>KP_Subtract", ZOOM_OUT, self._zoomOutCb), ) selection_actions = ( ("DeleteObj", Gtk.STOCK_DELETE, None, "Delete", DELETE, self._deleteSelected), ("UngroupObj", "pitivi-ungroup", _("Ungroup"), "<Shift><Control>G", UNGROUP, self._ungroupSelected), # Translators: This is an action, the title of a button ("GroupObj", "pitivi-group", _("Group"), "<Control>G", GROUP, self._groupSelected), ("AlignObj", "pitivi-align", _("Align"), "<Shift><Control>A", ALIGN, self._alignSelected), ) playhead_actions = ( ("PlayPause", Gtk.STOCK_MEDIA_PLAY, None, "space", _("Start Playback"), self._playPause), ("Split", "pitivi-split", _("Split"), "S", SPLIT, self._split), ("Keyframe", "pitivi-keyframe", _("Add a Keyframe"), "K", KEYFRAME, self._keyframe), ("Prevkeyframe", None, _("_Previous Keyframe"), "comma", PREVKEYFRAME, self._previousKeyframeCb), ("Nextkeyframe", None, _("_Next Keyframe"), "period", NEXTKEYFRAME, self._nextKeyframeCb), ) actiongroup = Gtk.ActionGroup("timelinepermanent") self.selection_actions = Gtk.ActionGroup("timelineselection") self.playhead_actions = Gtk.ActionGroup("timelineplayhead") actiongroup.add_actions(actions) self.ui_manager.insert_action_group(actiongroup, 0) self.selection_actions.add_actions(selection_actions) self.selection_actions.set_sensitive(False) self.ui_manager.insert_action_group(self.selection_actions, -1) self.playhead_actions.add_actions(playhead_actions) self.ui_manager.insert_action_group(self.playhead_actions, -1) self.ui_manager.add_ui_from_string(ui) def _packScrollbars(self, vbox): self.hadj = Gtk.Adjustment() self.vadj = Gtk.Adjustment() self._vscrollbar = Gtk.VScrollbar(self.vadj) self._hscrollBar = Gtk.HScrollbar(self.hadj) self.ruler = ScaleRuler(self, self.hadj) hbox = Gtk.HBox() self.hadj.connect("value-changed", self._updateScrollPosition) self.vadj.connect("value-changed", self._updateScrollPosition) vbox.pack_end(self._hscrollBar, False, True, False) self.ruler.setProjectFrameRate(24.) self.ruler.set_size_request(0, 25) self.ruler.hide() self.vadj.props.lower = 0 self.vadj.props.page_size = 250 hbox.pack_start(self.embed, True, True, True) hbox.pack_start(self._vscrollbar, False, True, False) vbox.pack_end(hbox, True, True, True) hbox = Gtk.HBox() self.zoomBox.set_size_request(CONTROL_WIDTH, -1) hbox.pack_start(self.zoomBox, False, True, False) hbox.pack_start(self.ruler, True, True, True) vbox.pack_end(hbox, False, True, False) def _updateScrollPosition(self, adjustment): self._scroll_pos_ns = Zoomable.pixelToNs(self.hadj.get_value()) point = Clutter.Point() point.x = self.hadj.get_value() point.y = self.vadj.get_value() self.point = point self.timeline.scroll_to_point(point) point.x = 0 self.controls.scroll_to_point(point) def _setBestZoomRatio(self): """ Set the zoom level so that the entire timeline is in view. """ ruler_width = self.ruler.get_allocation().width # Add Gst.SECOND - 1 to the timeline duration to make sure the # last second of the timeline will be in view. duration = self.timeline.bTimeline.get_duration() if duration == 0: return timeline_duration = duration + Gst.SECOND - 1 timeline_duration_s = int(timeline_duration / Gst.SECOND) ideal_zoom_ratio = float(ruler_width) / timeline_duration_s nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio) Zoomable.setZoomLevel(nearest_zoom_level) self.timeline.bTimeline.props.snapping_distance = \ Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband) # Only do this at the very end, after updating the other widgets. self.zoomed_fitted = True def _scrollLeft(self): self._hscrollBar.set_value(self._hscrollBar.get_value() - self.hadj.props.page_size ** (2.0 / 3.0)) def _scrollRight(self): self._hscrollBar.set_value(self._hscrollBar.get_value() + self.hadj.props.page_size ** (2.0 / 3.0)) def _scrollUp(self): self._vscrollbar.set_value(self._vscrollbar.get_value() - self.vadj.props.page_size ** (2.0 / 3.0)) def _scrollDown(self): self._vscrollbar.set_value(self._vscrollbar.get_value() + self.vadj.props.page_size ** (2.0 / 3.0)) def _scrollToPosition(self, position): if self.pipeline and self.pipeline.get_state() != Gst.State.PLAYING: self.timeline.save_easing_state() self.timeline.set_easing_duration(600) self._hscrollBar.set_value(position) if self.pipeline and self.pipeline.get_state() != Gst.State.PLAYING: self.timeline.restore_easing_state() return False def _scrollToPlayhead(self): #self.ruler._maybeUpdate() if self.ruler.pressed or self.pressed: self.pressed = False return canvas_size = self.embed.get_allocation().width - CONTROL_WIDTH try: new_pos = Zoomable.nsToPixel(self.app.current.pipeline.getPosition()) except PipelineError: return except AttributeError: # Standalone, no pipeline. return scroll_pos = self.hadj.get_value() self.scrollToPosition(min(new_pos - canvas_size / 2, self.hadj.props.upper - canvas_size - 1)) def _deleteSelected(self, unused_action): if self.timeline: self.app.action_log.begin("delete clip") #FIXME GES port: Handle unlocked TrackElement-s for clip in self.timeline.selection: layer = clip.get_layer() layer.remove_clip(clip) self.app.action_log.commit() def _ungroupSelected(self, unused_action): if self.timeline: self.timeline.enable_update(False) self.app.action_log.begin("ungroup") for clip in self.timeline.selection: clip.ungroup(False) self.timeline.enable_update(True) self.app.action_log.commit() def _groupSelected(self, unused_action): if self.timeline: self.timeline.enable_update(False) self.app.action_log.begin("group") GES.Container.group(self.timeline.selection) self.app.action_log.commit() self.timeline.enable_update(True) def _alignSelected(self, unused_action): if "NumPy" in missing_soft_deps: DepsManager(self.app) elif self.timeline: progress_dialog = AlignmentProgressDialog(self.app) progress_dialog.window.show() self.app.action_log.begin("align") self.timeline.enable_update(False) def alignedCb(): # Called when alignment is complete self.timeline.enable_update(True) self.app.action_log.commit() progress_dialog.window.destroy() pmeter = self.timeline.alignSelection(alignedCb) pmeter.addWatcher(progress_dialog.updatePosition) def _split(self, action): """ Split clips at the current playhead position, regardless of selections. """ self.bTimeline.enable_update(False) position = self.app.current.pipeline.getPosition() for track in self.bTimeline.get_tracks(): for element in track.get_elements(): start = element.get_start() end = start + element.get_duration() if start < position and end > position: clip = element.get_parent() clip.split(position) self.bTimeline.enable_update(True) def _keyframe(self, action): """ Add or remove a keyframe at the current position of the selected clip. FIXME GES: this method is currently not used anywhere """ selected = self.timeline.selection.getSelectedTrackElements() for obj in selected: keyframe_exists = False position = self.app.current.pipeline.getPosition() position_in_obj = (position - obj.start) + obj.in_point interpolators = obj.getInterpolators() for value in interpolators: interpolator = obj.getInterpolator(value) keyframes = interpolator.getInteriorKeyframes() for kf in keyframes: if kf.getTime() == position_in_obj: keyframe_exists = True self.app.action_log.begin("remove volume point") interpolator.removeKeyframe(kf) self.app.action_log.commit() if keyframe_exists is False: self.app.action_log.begin("add volume point") interpolator.newKeyframe(position_in_obj) self.app.action_log.commit() def _playPause(self, unused_action): self.app.current.pipeline.togglePlayback() def _transposeXY(self, x, y): height = self.ruler.get_allocation().height x += self.timeline.get_scroll_point().x return x - CONTROL_WIDTH, y - height # Interface # Zoomable def zoomChanged(self): if self._settings and self.bTimeline: # zoomChanged might be called various times before the UI is ready self.bTimeline.props.snapping_distance = \ Zoomable.pixelToNs(self._settings.edgeSnapDeadband) self.updateHScrollAdjustments() # Callbacks def _enterNotifyEventCb(self, widget, event): if self.gui: self.gui.setActionsSensitive(True) def _keyPressEventCb(self, widget, event): if event.keyval == Gdk.KEY_Shift_L: self.shiftMask = True elif event.keyval == Gdk.KEY_Control_L: self.controlMask = True def _keyReleaseEventCb(self, widget, event): if event.keyval == Gdk.KEY_Shift_L: self.shiftMask = False elif event.keyval == Gdk.KEY_Control_L: self.controlMask = False def _clickedCb(self, stage, event): self.pressed = True position = self.pixelToNs(event.x - CONTROL_WIDTH + self.timeline._scroll_point.x) if self.app: self._seeker.seek(position) actor = self.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, event.x, event.y) if actor == stage: self.timeline.emptySelection() def _releasedCb(self, stage, event): self.timeline._snapEndedCb() def _renderingSettingsChangedCb(self, project, item, value): """ Called when any Project metadata changes, we filter out the one we are interested in. if @item is None, it mean we called it ourself, and want to force getting the project videorate value """ if item == "videorate" or item is None: if value is None: value = project.videorate self._framerate = value self.ruler.setProjectFrameRate(self._framerate) def _snapDistanceChangedCb(self, settings): if self.bTimeline: self.bTimeline.props.snapping_distance = \ Zoomable.pixelToNs(settings.edgeSnapDeadband) def _projectChangedCb(self, app, project, unused_fully_loaded): """ When a project is loaded, we connect to its pipeline """ if project: self._seeker = self._project.seeker self.timeline.setPipeline(self._project.pipeline) self.ruler.setProjectFrameRate(self._project.videorate) self.ruler.zoomChanged() self._renderingSettingsChangedCb(self._project, None, None) self._setBestZoomRatio() def _projectCreatedCb(self, app, project): """ When a project is created, we connect to it timeline """ if self._project: self._project.disconnect_by_func(self._renderingSettingsChangedCb) try: self.timeline.pipeline.disconnect_by_func(self.timeline.positionCb) except AttributeError: pass except TypeError: pass # We were not connected no problem self.timeline.pipeline = None self._seeker = None self._project = project if self._project: self._project.connect("rendering-settings-changed", self._renderingSettingsChangedCb) self.setTimeline(project.timeline) def _zoomInCb(self, unused_action): Zoomable.zoomIn() self.log("Setting 'zoomed_fitted' to False") self.zoomed_fitted = False def _zoomOutCb(self, unused_action): Zoomable.zoomOut() self.log("Setting 'zoomed_fitted' to False") self.zoomed_fitted = False def _zoomFitCb(self, unused, unsued2=None): self._setBestZoomRatio() def _screenshotCb(self, unused_action): """ Export a snapshot of the current frame as an image file. """ foo = self._showSaveScreenshotDialog() if foo: path, mime = foo[0], foo[1] self._project.pipeline.save_thumbnail(-1, -1, mime, path) def _previousKeyframeCb(self, action): position = self.app.current.pipeline.getPosition() prev_kf = self.timeline.getPrevKeyframe(position) if prev_kf: self._seeker.seek(prev_kf) self.scrollToPlayhead() def _nextKeyframeCb(self, action): position = self.app.current.pipeline.getPosition() next_kf = self.timeline.getNextKeyframe(position) if next_kf: self._seeker.seek(next_kf) self.scrollToPlayhead() def _scrollEventCb(self, embed, event): # FIXME : see https://bugzilla.gnome.org/show_bug.cgi?id=697522 deltas = event.get_scroll_deltas() if event.state & Gdk.ModifierType.CONTROL_MASK: if deltas[2] < 0: Zoomable.zoomIn() elif deltas[2] > 0: Zoomable.zoomOut() self.zoomed_fitted = False self._scrollToPlayhead() elif event.state & Gdk.ModifierType.SHIFT_MASK: if deltas[2] > 0: self._scrollDown() elif deltas[2] < 0: self._scrollUp() else: if deltas[2] > 0: self._scrollRight() elif deltas[2] < 0: self._scrollLeft() self.scrolled += 1 def _selectionChangedCb(self, selection): """ The selected clips on the timeline canvas have changed with the "selection-changed" signal. This is where you apply global UI changes, unlike individual track elements' "selected-changed" signal from the Selected class. """ if selection: self.selection_actions.set_sensitive(True) else: self.selection_actions.set_sensitive(False) # drag and drop def _dragDataReceivedCb(self, widget, context, x, y, data, info, time): if not self.dropDataReady: if data.get_length() > 0: if not self.dropOccured: self.timeline.resetGhostClips() self.dropData = data.get_uris() self.dropDataReady = True if self.dropOccured: self.dropOccured = False Gtk.drag_finish(context, True, False, time) self._dragLeaveCb(widget, context, time) else: self.isDraggedClip = True def _dragDropCb(self, widget, context, x, y, time): target = widget.drag_dest_find_target(context, None) y -= self.ruler.get_allocation().height if target.name() == "text/uri-list": self.dropOccured = True widget.drag_get_data(context, target, time) if self.isDraggedClip: self.timeline.convertGhostClips() self.timeline.resetGhostClips() if self.zoomed_fitted: self._setBestZoomRatio() else: x, y = self._transposeXY(x, y) self.scrollToPosition(Zoomable.pixelToNs(x)) else: actor = self.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y) try: bElement = actor.bElement self.app.gui.clipconfig.effect_expander.addEffectToClip(bElement.get_parent(), self.dropData[0]) except AttributeError: return False return True else: return False def _dragMotionCb(self, widget, context, x, y, time): target = widget.drag_dest_find_target(context, None) if target.name() not in ["text/uri-list", "pitivi/effect"]: return False if not self.dropDataReady: widget.drag_get_data(context, target, time) Gdk.drag_status(context, 0, time) else: x, y = self._transposeXY(x, y) # dragged from the media library if not self.timeline.ghostClips and self.isDraggedClip: for uri in self.dropData: asset = self.app.gui.medialibrary.getAssetForUri(uri) if asset is None: self.isDraggedClip = False break self.timeline.addGhostClip(asset, x, y) if self.isDraggedClip: self.timeline.updateGhostClips(x, y) Gdk.drag_status(context, Gdk.DragAction.COPY, time) if not self.dropHighlight: widget.drag_highlight() self.dropHighlight = True return True def _dragLeaveCb(self, widget, context, time): if self.dropDataReady: self.dropDataReady = False if self.dropHighlight: widget.drag_unhighlight() self.dropHighlight = False self.timeline.removeGhostClips() # Standalone # Standalone public API def run(self): self.testTimeline(self.timeline) GLib.io_add_watch(sys.stdin, GLib.IO_IN, quit2_) Gtk.main() def addClipToLayer(self, layer, asset, start, duration, inpoint): layer.add_asset(asset, start * Gst.SECOND, 0, duration * Gst.SECOND, asset.get_supported_formats()) def togglePlayback(self, button): self.pipeline.togglePlayback() def testTimeline(self, timeline): timeline.set_easing_duration(600) Gst.init([]) GES.init() self.project = GES.Project(uri=None, extractable_type=GES.Timeline) bTimeline = GES.Timeline() bTimeline.add_track(GES.Track.audio_raw_new()) bTimeline.add_track(GES.Track.video_raw_new()) self.bTimeline = bTimeline timeline.setTimeline(bTimeline) layer = GES.Layer() bTimeline.add_layer(layer) self.bTimeline = bTimeline self.project.connect("asset-added", self._doAssetAddedCb, layer) self.project.create_asset("file://" + sys.argv[2], GES.UriClip) # Standalone internal API def _handle_message(self, bus, message): if message.type == Gst.MessageType.ELEMENT: if message.has_name('prepare-window-handle'): Gdk.threads_enter() self.sink = message.src self.sink.set_window_handle(self.viewer.window_xid) self.sink.expose() Gdk.threads_leave() elif message.type == Gst.MessageType.STATE_CHANGED: prev, new, pending = message.parse_state_changed() return True # Standalone callbacks def _doAssetAddedCb(self, project, asset, layer): self.addClipToLayer(layer, asset, 2, 10, 5) self.addClipToLayer(layer, asset, 15, 10, 5) Zoomable.setZoomLevel(50)