def render_cairo(self, cr, bounds, element, hscroll_pos, y1): if not self._view: return # The idea is to conceptually divide the clip into a sequence of # rectangles beginning at the start of the file, and # pixelsToNs(twidth) nanoseconds long. The thumbnail within the # rectangle is the frame produced from the timestamp corresponding to # rectangle's left edge. We speed things up by only drawing the # rectangles which intersect the given bounds. FIXME: how would we # handle timestretch? height = bounds.y2 - bounds.y1 width = bounds.x2 - bounds.x1 # we actually draw the rectangles just to the left of the clip's in # point and just to the right of the clip's out-point, so we need to # mask off the actual bounds. cr.rectangle(bounds.x1, bounds.y1, width, height) cr.clip() # tdur = duration in ns of thumbnail # sof = start of file in pixel coordinates x1 = bounds.x1 sof = Zoomable.nsToPixel(element.get_start() - element.get_inpoint()) +\ hscroll_pos # i = left edge of thumbnail to be drawn. We start with x1 and # subtract the distance to the nearest leftward rectangle. # Justification of the following: # i = sof + k * twidth # i = x1 - delta # sof + k * twidth = x1 - delta # i * tw = (x1 - sof) - delta # <=> delta = x1 - sof (mod twidth). # Fortunately for us, % works on floats in python. i = x1 - ((x1 - sof) % (self.twidth + self._spacing())) # j = timestamp *within the element* of thumbnail to be drawn. we want # timestamps to be numerically stable, but in practice this seems to # give good enough results. It might be possible to improve this # further, which would result in fewer thumbnails needing to be # generated. j = Zoomable.pixelToNs(i - sof) istep = self.twidth + self._spacing() jstep = self.tdur + Zoomable.pixelToNs(self.spacing) while i < bounds.x2: self._thumbForTime(cr, j, i, y1) cr.rectangle(i - 1, y1, self.twidth + 2, self.theight) i += istep j += jstep cr.fill()
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()
def convertGhostClips(self): for ghostCouple in self.ghostClips: ghostclip = ghostCouple[0] if not ghostclip: ghostclip = ghostCouple[1] layer = None target = None if ghostclip.shouldCreateLayer: layer = self.insertLayer(ghostclip) target = layer else: for layer in self.bTimeline.get_layers(): if layer.get_priority() == ghostclip.priority: target = layer break if target is None: layer = self.bTimeline.append_layer() layer.add_asset(ghostclip.asset, Zoomable.pixelToNs(ghostclip.props.x), 0, ghostclip.asset.get_duration(), ghostclip.asset.get_supported_formats())
def updateValue(self, delta_x, delta_y): newTs = self.tsStart + Zoomable.pixelToNs(delta_x) newValue = self.valueStart - (delta_y / EXPANDED_SIZE) # Don't overlap first and last keyframes. newTs = min(max(newTs, self.inpoint + 1), self.duration + self.inpoint - 1) newValue = min(max(newValue, 0.0), 1.0) if not self.has_changeable_time: newTs = self.lastTs self.timelineElement.source.unset(self.lastTs) if (self.timelineElement.source.set(newTs, newValue)): self.value = Gst.TimedValue() self.value.timestamp = newTs self.value.value = newValue self.lastTs = newTs self.timelineElement.setKeyframePosition(self, self.value) # Resort the keyframes list each time. Should be cheap as there should never be too much keyframes, # if optimization is needed, check if resorting is needed, should not be in 99 % of the cases. self.timelineElement.keyframes = sorted(self.timelineElement.keyframes, key=lambda keyframe: keyframe.value.timestamp) self.timelineElement.drawLines(self.line) # This will update the viewer. nifty. if not self.line: self.timelineElement.timeline._container.seekInPosition(newTs + self.start)
def updateValue(self, delta_x, delta_y): newTs = self.tsStart + Zoomable.pixelToNs(delta_x) newValue = self.valueStart - (delta_y / EXPANDED_SIZE) # Don't overlap first and last keyframes. newTs = min(max(newTs, self.inpoint + 1), self.duration + self.inpoint - 1) newValue = min(max(newValue, 0.0), 1.0) if not self.has_changeable_time: newTs = self.lastTs updating = self.timelineElement.updating_keyframes self.timelineElement.updating_keyframes = True self.timelineElement.source.unset(self.lastTs) if (self.timelineElement.source.set(newTs, newValue)): self.value = Gst.TimedValue() self.value.timestamp = newTs self.value.value = newValue self.lastTs = newTs self.timelineElement.setKeyframePosition(self, self.value) # Resort the keyframes list each time. Should be cheap as there should never be too much keyframes, # if optimization is needed, check if resorting is needed, should # not be in 99 % of the cases. self.timelineElement.keyframes = sorted( self.timelineElement.keyframes, key=lambda keyframe: keyframe.value.timestamp) self.timelineElement.drawLines(self.line) # This will update the viewer. nifty. if not self.line: self.timelineElement.timeline._container.seekInPosition( newTs + self.start) self.timelineElement.updating_keyframes = updating
def _clickedCb(self, unused_actor, event): if self.gotDragged: self.gotDragged = False return x, unused_y = self.transposeXY(event.x, event.y) timestamp = Zoomable.pixelToNs(x) value = self._valueAtTimestamp(timestamp) self.timelineElement.addKeyframe(value, timestamp)
def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period)
def _get_thumb_duration(self): thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin) # quantize thumb length to thumb_period thumb_duration = quantize(thumb_duration_tmp, self.thumb_period) # make sure that the thumb duration after the quantization isn't smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period return max(thumb_duration, self.thumb_period)
def _dragProgressCb(self, unused_action, unused_actor, delta_x, unused_delta_y): # We can't use delta_x here because it fluctuates weirdly. coords = self.dragAction.get_motion_coords() delta_x = coords[0] - self.dragBeginStartX new_start = self._dragBeginStart + Zoomable.pixelToNs(delta_x) self._context.setMode(self.timelineElement.timeline._container.getEditionMode(isAHandle=True)) self._context.editTo(new_start, self.timelineElement.bElement.get_parent().get_layer().get_priority()) return 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 xyToTimeValue(self, pos): view = self._view interpolator = view.interpolator bounds = view.bounds time = (Zoomable.pixelToNs(pos[0] - bounds.x1) + view.element.in_point) value = ((1 - (pos[1] - KW_LABEL_Y_OVERFLOW - bounds.y1 - view._min) / view._range) * interpolator.range) + interpolator.lower return time, value
def _clickedCb(self, actor, event): if self.gotDragged: self.gotDragged = False return x, y = self.transposeXY(event.x, event.y) value = 1.0 - (y / EXPANDED_SIZE) value = max(0.0, value) value = min(1.0, value) timestamp = Zoomable.pixelToNs(x) self.timelineElement.addKeyframe(value, timestamp)
def drag_start(self, item, target, event): """ Start draging an element in the Track """ self.debug("Drag started") if not self._view.element.selected: self._view.timeline.selection.setToObj(self._view.element, SELECT) if self.previous_x is not None: ratio = float(self.ref / Zoomable.pixelToNs(10000000000)) self.previous_x = self.previous_x * ratio self.ref = Zoomable.pixelToNs(10000000000) tx = self._view.props.parent.get_simple_transform() # store y offset for later priority calculation self._y_offset = tx[4] # zero y component of mousdown coordiante self._mousedown = Point(self._mousedown[0], 0)
def _sliderTooltipCb(self, unused_slider, unused_x, unused_y, unused_keyboard_mode, tooltip): # We assume the width of the ruler is exactly the width of the timeline. width_px = self.timeline.ruler.get_allocated_width() timeline_width_ns = Zoomable.pixelToNs(width_px) if timeline_width_ns >= Gst.SECOND: # Translators: %s represents a duration, for example "10 minutes" tip = _("%s displayed") % beautify_length(timeline_width_ns) else: # Translators: This is a tooltip tip = _("%d nanoseconds displayed, because we can") % timeline_width_ns tooltip.set_text(tip) return True
def set_pos(self, item, pos): if not self._view.movable: return x, y = pos x = x + self._hadj.get_value() position = Zoomable.pixelToNs(x) priority = self.app.gui.timeline_ui.controls.getPriorityForY( y - self._y_offset + self._vadj.get_value()) self._context.setMode(self._getMode()) self.debug("Setting position") self._context.editTo(position, priority)
def _get_visible_timeline_range(self): # determine the visible left edge of the timeline # TODO: isn't there some easier way to get the scroll point of the ScrollActor? # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x) timeline_left = self.timeline.get_scroll_point().x # determine the width of the pipeline # by intersecting the timeline's and the stage's allocation timeline_allocation = self.timeline.props.allocation stage_allocation = self.timeline.get_stage().props.allocation timeline_rect = Clutter.Rect() timeline_rect.init(timeline_allocation.x1, timeline_allocation.y1, timeline_allocation.x2 - timeline_allocation.x1, timeline_allocation.y2 - timeline_allocation.y1) stage_rect = Clutter.Rect() stage_rect.init(stage_allocation.x1, stage_allocation.y1, stage_allocation.x2 - stage_allocation.x1, stage_allocation.y2 - stage_allocation.y1) has_intersection, intersection = timeline_rect.intersection(stage_rect) if not has_intersection: return (0, 0) timeline_width = intersection.size.width # determine the visible right edge of the timeline timeline_right = timeline_left + timeline_width # convert to nanoseconds time_left = Zoomable.pixelToNs(timeline_left) time_right = Zoomable.pixelToNs(timeline_right) return (time_left, time_right)
def _sliderTooltipCb(self, unused_slider, unused_x, unused_y, unused_keyboard_mode, tooltip): # We assume the width of the ruler is exactly the width of the # timeline. width_px = self.timeline.ruler.get_allocated_width() timeline_width_ns = Zoomable.pixelToNs(width_px) if timeline_width_ns >= Gst.SECOND: # Translators: %s represents a duration, for example "10 minutes" tip = _("%s displayed") % beautify_length(timeline_width_ns) else: # Translators: This is a tooltip tip = _( "%d nanoseconds displayed, because we can") % timeline_width_ns tooltip.set_text(tip) return True
def thumb_interval(self): """Gets the interval for which a thumbnail is displayed. Returns: int: a duration in nanos, multiple of THUMB_PERIOD. """ interval = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX) # Make sure the thumb interval is a multiple of THUMB_PERIOD. quantized = quantize(interval, THUMB_PERIOD) # Make sure the quantized thumb interval fits # the thumb and the margin. if quantized < interval: quantized += THUMB_PERIOD # Make sure we don't show thumbs more often than THUMB_PERIOD. return max(THUMB_PERIOD, quantized)
def click(self, pos): timeline = self._view.timeline element = self._view.element if self._last_event.get_state()[1] & Gdk.ModifierType.SHIFT_MASK: timeline.selection.setToObj(element, SELECT_BETWEEN) elif self._last_event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK: if element.selected: mode = UNSELECT else: mode = SELECT_ADD timeline.selection.setToObj(element, mode) else: x, y = pos x += self._hadj.get_value() self._view.app.current.seeker.seek(Zoomable.pixelToNs(x)) timeline.selection.setToObj(element, SELECT)
def _addVisibleThumbnails(self): """ Get the thumbnails to be displayed in the currently visible clip portion """ self.remove_all_children() old_thumbs = self.thumbs.copy() self.thumbs = {} self.wishlist = [] thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin) # quantize thumb length to thumb_period # TODO: replace with a call to utils.misc.quantize: thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period # make sure that the thumb duration after the quantization isn't smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period thumb_duration = max(thumb_duration, self.thumb_period) element_left, element_right = self._get_visible_range() # TODO: replace with a call to utils.misc.quantize: element_left = (element_left // thumb_duration) * thumb_duration current_time = element_left while current_time < element_right: thumb = Thumbnail(self.thumb_width, self.thumb_height) thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin) self.add_child(thumb) self.thumbs[current_time] = thumb if current_time in self.thumb_cache: gdkpixbuf = self.thumb_cache[current_time] if self._allAnimated or current_time not in old_thumbs: self.thumbs[current_time].set_from_gdkpixbuf_animated( gdkpixbuf) else: self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf) else: self.wishlist.append(current_time) current_time += thumb_duration self._allAnimated = False
def _addVisibleThumbnails(self): """ Get the thumbnails to be displayed in the currently visible clip portion """ self.remove_all_children() old_thumbs = self.thumbs.copy() self.thumbs = {} self.wishlist = [] thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin) # quantize thumb length to thumb_period # TODO: replace with a call to utils.misc.quantize: thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period # make sure that the thumb duration after the quantization isn't smaller than before if thumb_duration < thumb_duration_tmp: thumb_duration += self.thumb_period # make sure that we don't show thumbnails more often than thumb_period thumb_duration = max(thumb_duration, self.thumb_period) element_left, element_right = self._get_visible_range() # TODO: replace with a call to utils.misc.quantize: element_left = (element_left // thumb_duration) * thumb_duration current_time = element_left while current_time < element_right: thumb = Thumbnail(self.thumb_width, self.thumb_height) thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin) self.add_child(thumb) self.thumbs[current_time] = thumb if current_time in self.thumb_cache: gdkpixbuf = self.thumb_cache[current_time] if self._allAnimated or current_time not in old_thumbs: self.thumbs[current_time].set_from_gdkpixbuf_animated(gdkpixbuf) else: self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf) else: self.wishlist.append(current_time) current_time += thumb_duration self._allAnimated = False
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 _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 _snapDistanceChangedCb(self, settings): if self.bTimeline: self.bTimeline.props.snapping_distance = \ Zoomable.pixelToNs(settings.edgeSnapDeadband)
def tdur(self): return Zoomable.pixelToNs(self.twidth)
def check_keyframe_ui_toggle(self, ges_clip, timeline_container): """Checks keyframes toggling by click events.""" timeline = timeline_container.timeline start = ges_clip.props.start start_px = Zoomable.nsToPixel(start) inpoint = ges_clip.props.in_point duration = ges_clip.props.duration duration_px = Zoomable.nsToPixel(duration) offsets_px = (1, int(duration_px / 2), int(duration_px) - 1) timeline.selection.select([ges_clip]) ges_video_source = ges_clip.find_track_element(None, GES.VideoSource) binding = ges_video_source.get_control_binding("alpha") control_source = binding.props.control_source keyframe_curve = ges_video_source.ui.keyframe_curve values = [item.timestamp for item in control_source.get_all()] self.assertEqual(values, [inpoint, inpoint + duration]) # Add keyframes by simulating mouse clicks. for offset_px in offsets_px: offset = Zoomable.pixelToNs(start_px + offset_px) - start xdata, ydata = inpoint + offset, 1 x, y = keyframe_curve._ax.transData.transform((xdata, ydata)) event = MouseEvent( name="button_press_event", canvas=keyframe_curve, x=x, y=y, button=1 ) keyframe_curve.translate_coordinates = \ mock.Mock(return_value=(start_px+offset_px, None)) with mock.patch.object(Gtk, "get_event_widget") as get_event_widget: get_event_widget.return_value = keyframe_curve event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.name = "button_release_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) keyframe_curve._mpl_button_release_event_cb(event) values = [item.timestamp for item in control_source.get_all()] self.assertIn(inpoint + offset, values) # Remove keyframes by simulating mouse double-clicks. for offset_px in offsets_px: offset = Zoomable.pixelToNs(start_px + offset_px) - start xdata, ydata = inpoint + offset, 1 x, y = keyframe_curve._ax.transData.transform((xdata, ydata)) event = MouseEvent( name="button_press_event", canvas=keyframe_curve, x=x, y=y, button=1 ) keyframe_curve.translate_coordinates = \ mock.Mock(return_value=(start_px + offset_px, None)) with mock.patch.object(Gtk, "get_event_widget") as get_event_widget: get_event_widget.return_value = keyframe_curve event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.name = "button_release_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) keyframe_curve._mpl_button_release_event_cb(event) event.name = "button_press_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.guiEvent = Gdk.Event.new(Gdk.EventType._2BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.name = "button_release_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) keyframe_curve._mpl_button_release_event_cb(event) values = [item.timestamp for item in control_source.get_all()] self.assertNotIn(inpoint + offset, values)
def check_keyframe_ui_toggle(self, ges_clip, timeline_container): """Checks keyframes toggling by click events.""" timeline = timeline_container.timeline start = ges_clip.props.start start_px = Zoomable.nsToPixel(start) inpoint = ges_clip.props.in_point duration = ges_clip.props.duration duration_px = Zoomable.nsToPixel(duration) offsets_px = (1, int(duration_px / 2), int(duration_px) - 1) timeline.selection.select([ges_clip]) ges_video_source = ges_clip.find_track_element(None, GES.VideoSource) binding = ges_video_source.get_control_binding("alpha") control_source = binding.props.control_source keyframe_curve = ges_video_source.ui.keyframe_curve values = [item.timestamp for item in control_source.get_all()] self.assertEqual(values, [inpoint, inpoint + duration]) # Add keyframes by simulating mouse clicks. for offset_px in offsets_px: offset = Zoomable.pixelToNs(start_px + offset_px) - start xdata, ydata = inpoint + offset, 1 x, y = keyframe_curve._ax.transData.transform((xdata, ydata)) event = MouseEvent(name="button_press_event", canvas=keyframe_curve, x=x, y=y, button=1) keyframe_curve.translate_coordinates = \ mock.Mock(return_value=(start_px + offset_px, None)) with mock.patch.object(Gtk, "get_event_widget") as get_event_widget: get_event_widget.return_value = keyframe_curve event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.name = "button_release_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) keyframe_curve._mpl_button_release_event_cb(event) values = [item.timestamp for item in control_source.get_all()] self.assertIn(inpoint + offset, values) # Remove keyframes by simulating mouse double-clicks. for offset_px in offsets_px: offset = Zoomable.pixelToNs(start_px + offset_px) - start xdata, ydata = inpoint + offset, 1 x, y = keyframe_curve._ax.transData.transform((xdata, ydata)) event = MouseEvent(name="button_press_event", canvas=keyframe_curve, x=x, y=y, button=1) keyframe_curve.translate_coordinates = \ mock.Mock(return_value=(start_px + offset_px, None)) with mock.patch.object(Gtk, "get_event_widget") as get_event_widget: get_event_widget.return_value = keyframe_curve event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.name = "button_release_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) keyframe_curve._mpl_button_release_event_cb(event) event.name = "button_press_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.guiEvent = Gdk.Event.new(Gdk.EventType._2BUTTON_PRESS) keyframe_curve._mpl_button_press_event_cb(event) event.name = "button_release_event" event.guiEvent = Gdk.Event.new(Gdk.EventType.BUTTON_RELEASE) keyframe_curve._mpl_button_release_event_cb(event) values = [item.timestamp for item in control_source.get_all()] self.assertNotIn(inpoint + offset, values)