def __init__(self, instance, hadj): Gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") self.app = instance self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.min_frame_spacing = 5.0 self.frame_height = 5.0 self.frame_rate = Gst.Fraction(1 / 1) self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.connect('draw', self.drawCb) self.connect('configure-event', self.configureEventCb) self.callback_id = None self.callback_id_scroll = None
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 __init__(self, settings=None, realizedCb=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=1.0, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.drawing_area = Gtk.DrawingArea() self.drawing_area.set_double_buffered(False) self.drawing_area.connect("draw", self._drawCb, None) # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! if realizedCb: self.drawing_area.connect("realize", realizedCb, self) self.add(self.drawing_area) self.drawing_area.show() self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None
def __init__(self, instance, uimap): Loggable.__init__(self) Signallable.__init__(self) self.app = instance self.bt = {} self.settings = {} self.source = None self.created = False self.seeker = Seeker() #Drag attributes self._drag_events = [] self._signals_connected = False self._createUI() self.textbuffer = Gtk.TextBuffer() self.pangobuffer = InteractivePangoBuffer() self.textarea.set_buffer(self.pangobuffer) self.textbuffer.connect("changed", self._updateSourceText) self.pangobuffer.connect("changed", self._updateSourceText) #Connect buttons self.pangobuffer.setup_widget_from_pango(self.bt["bold"], "<b>bold</b>") self.pangobuffer.setup_widget_from_pango(self.bt["italic"], "<i>italic</i>")
def __init__(self, app): Loggable.__init__(self) self.app = app self.settings = {} self.source = None self.seeker = Seeker() # Drag attributes self._drag_events = [] self._signals_connected = False self._createUI()
def __init__(self, settings=None): Gtk.DrawingArea.__init__(self) Loggable.__init__(self) self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None
def __init__(self, timeline, hadj): Gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") # Allows stealing focus from other GTK widgets, prevent accidents: self.props.can_focus = True self.connect("focus-in-event", self._focusInCb) self.connect("focus-out-event", self._focusOutCb) self.timeline = timeline self._background_color = timeline.get_style_context().lookup_color( 'theme_bg_color')[1] self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.frame_rate = Gst.Fraction(1 / 1) self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.connect('draw', self.drawCb) self.connect('configure-event', self.configureEventCb) self.callback_id = None self.callback_id_scroll = None self.set_size_request(0, HEIGHT) style = self.get_style_context() color_normal = style.get_color(Gtk.StateFlags.NORMAL) color_insensitive = style.get_color(Gtk.StateFlags.INSENSITIVE) self._color_normal = color_normal self._color_dimmed = Gdk.RGBA( *[(x * 3 + y * 2) / 5 for x, y in ((color_normal.red, color_insensitive.red), (color_normal.green, color_insensitive.green), (color_normal.blue, color_insensitive.blue))]) self.scales = SCALES
def __init__(self, app): Loggable.__init__(self) self.app = app self.action_log = app.action_log self.settings = {} self.source = None self.seeker = Seeker() # Drag attributes self._drag_events = [] self._signals_connected = False self._setting_props = False self._setting_initial_props = False self._children_props_handler = None self._createUI()
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 __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()
def __init__(self, settings=None, realizedCb=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=1.0, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.drawing_area = GtkClutter.Embed() self.drawing_area.set_double_buffered(False) # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! if realizedCb: self.drawing_area.connect("realize", realizedCb, self) self.add(self.drawing_area) layout_manager = Clutter.BinLayout(x_align=Clutter.BinAlignment.FILL, y_align=Clutter.BinAlignment.FILL) self.drawing_area.get_stage().set_layout_manager(layout_manager) self.texture = Clutter.Texture() # This is a trick to make the viewer appear darker at the start. self.texture.set_from_rgb_data(data=[0] * 3, has_alpha=False, width=1, height=1, rowstride=3, bpp=3, flags=Clutter.TextureFlags.NONE) self.drawing_area.get_stage().add_child(self.texture) self.drawing_area.show() self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None
def __init__(self, settings=None, realizedCb=None, sink=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=0.5, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.drawing_area = Pitivi.viewer_new(sink) self.drawing_area.set_double_buffered(False) self.drawing_area.connect("draw", self._drawCb, None) # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! if realizedCb: self.drawing_area.connect("realize", realizedCb, self) self.add(self.drawing_area) self.drawing_area.show() self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = sink self.pixbuf = None self.pipeline = None self.transformation_properties = None self._setting_ratio = False
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 __init__(self, instance, hadj): gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") self.app = instance self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.need_update = True self.min_frame_spacing = 5.0 self.frame_height = 5.0 self.frame_rate = gst.Fraction(1 / 1)
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()
def __init__(self, settings=None): gtk.DrawingArea.__init__(self) Loggable.__init__(self) self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None for state in range(gtk.STATE_INSENSITIVE + 1): self.modify_bg(state, self.style.black)
def __init__(self, sink, app=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=0.5, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.__transformationBox = TransformationBox(app) # We only work with a gtkglsink inside a glsinkbin try: self.drawing_area = sink.props.sink.props.widget except AttributeError: self.drawing_area = sink.props.widget # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! self.add(self.__transformationBox) self.__transformationBox.add(self.drawing_area) self.drawing_area.show() self.seeker = Seeker() if app: self.settings = app.settings self.app = app self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = sink self.pixbuf = None self.pipeline = None self.transformation_properties = None self._setting_ratio = False self.__startDraggingPosition = None self.__startEditSourcePosition = None self.__editSource = None
class ViewerWidget(Gtk.AspectFrame, Loggable): """ Widget for displaying a GStreamer video sink. @ivar settings: The settings of the application. @type settings: L{GlobalSettings} """ __gsignals__ = {} def __init__(self, sink, app=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=0.5, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.__transformationBox = TransformationBox(app) # We only work with a gtkglsink inside a glsinkbin try: self.drawing_area = sink.props.sink.props.widget except AttributeError: self.drawing_area = sink.props.widget # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! self.add(self.__transformationBox) self.__transformationBox.add(self.drawing_area) self.drawing_area.show() self.seeker = Seeker() if app: self.settings = app.settings self.app = app self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = sink self.pixbuf = None self.pipeline = None self.transformation_properties = None self._setting_ratio = False self.__startDraggingPosition = None self.__startEditSourcePosition = None self.__editSource = None def setDisplayAspectRatio(self, ratio): self._setting_ratio = True self.set_property("ratio", float(ratio)) def _sizeCb(self, unused_widget, unused_area): # The transformation box is cleared when using regular rendering # so we need to flush the pipeline self.seeker.flush() def do_get_preferred_width(self): # Do not let a chance for Gtk to choose video natural size # as we want to have full control return 0, 1 def do_get_preferred_height(self): # Do not let a chance for Gtk to choose video natural size # as we want to have full control return 0, 1
class TitleEditor(Loggable): def __init__(self, instance, uimap): Loggable.__init__(self) Signallable.__init__(self) self.app = instance self.bt = {} self.settings = {} self.source = None self.created = False self.seeker = Seeker() #Drag attributes self._drag_events = [] self._signals_connected = False self._createUI() self.textbuffer = Gtk.TextBuffer() self.pangobuffer = InteractivePangoBuffer() self.textarea.set_buffer(self.pangobuffer) self.textbuffer.connect("changed", self._updateSourceText) self.pangobuffer.connect("changed", self._updateSourceText) #Connect buttons self.pangobuffer.setup_widget_from_pango(self.bt["bold"], "<b>bold</b>") self.pangobuffer.setup_widget_from_pango(self.bt["italic"], "<i>italic</i>") def _createUI(self): builder = Gtk.Builder() builder.add_from_file(os.path.join(get_ui_dir(), "titleeditor.ui")) builder.connect_signals(self) self.widget = builder.get_object("box1") # To be used by tabsmanager self.infobar = builder.get_object("infobar") self.editing_box = builder.get_object("editing_box") self.textarea = builder.get_object("textview") self.markup_button = builder.get_object("markupToggle") toolbar = builder.get_object("toolbar") toolbar.get_style_context().add_class("inline-toolbar") buttons = ["bold", "italic", "font", "font_fore_color", "back_color"] for button in buttons: self.bt[button] = builder.get_object(button) settings = ["valignment", "halignment", "xpos", "ypos"] for setting in settings: self.settings[setting] = builder.get_object(setting) for n, en in {_("Custom"): "position", _("Top"): "top", _("Center"): "center", _("Bottom"): "bottom", _("Baseline"): "baseline"}.items(): self.settings["valignment"].append(en, n) for n, en in {_("Custom"): "position", _("Left"): "left", _("Center"): "center", _("Right"): "right"}.items(): self.settings["halignment"].append(en, n) self._deactivate() def _textviewFocusedCb(self, unused_widget, unused_event): self.app.gui.setActionsSensitive(False) def _textviewUnfocusedCb(self, unused_widget, unused_event): self.app.gui.setActionsSensitive(True) def _backgroundColorButtonCb(self, widget): self.textarea.modify_base(self.textarea.get_state(), widget.get_color()) color = widget.get_rgba() color_int = 0 color_int += int(color.red * 255) * 256 ** 2 color_int += int(color.green * 255) * 256 ** 1 color_int += int(color.blue * 255) * 256 ** 0 color_int += int(color.alpha * 255) * 256 ** 3 self.debug("Setting title background color to %s", hex(color_int)) self.source.set_background(color_int) def _frontTextColorButtonCb(self, widget): suc, a, t, s = Pango.parse_markup("<span color='" + widget.get_color().to_string() + "'>color</span>", -1, u'\x00') ai = a.get_iterator() font, lang, attrs = ai.get_font() tags = self.pangobuffer.get_tags_from_attrs(None, None, attrs) self.pangobuffer.apply_tag_to_selection(tags[0]) def _fontButtonCb(self, widget): font_desc = widget.get_font_name().split(" ") font_face = " ".join(font_desc[:-1]) font_size = str(int(font_desc[-1]) * 1024) text = "<span face='" + font_face + "'><span size='" + font_size + "'>text</span></span>" suc, a, t, s = Pango.parse_markup(text, -1, u'\x00') ai = a.get_iterator() font, lang, attrs = ai.get_font() tags = self.pangobuffer.get_tags_from_attrs(font, None, attrs) for tag in tags: self.pangobuffer.apply_tag_to_selection(tag) def _markupToggleCb(self, markup_button): # FIXME: either make this feature rock-solid or replace it by a # Clear markup" button. Currently it is possible for the user to create # invalid markup (causing errors) or to get our textbuffer confused self.textbuffer.disconnect_by_func(self._updateSourceText) self.pangobuffer.disconnect_by_func(self._updateSourceText) if markup_button.get_active(): self.textbuffer.set_text(self.pangobuffer.get_text()) self.textarea.set_buffer(self.textbuffer) for name in self.bt: self.bt[name].set_sensitive(False) else: txt = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) self.pangobuffer.set_text(txt) self.textarea.set_buffer(self.pangobuffer) for name in self.bt: self.bt[name].set_sensitive(True) self.textbuffer.connect("changed", self._updateSourceText) self.pangobuffer.connect("changed", self._updateSourceText) def _activate(self): """ Show the title editing UI widgets and hide the infobar """ self.infobar.hide() self.textarea.show() self.editing_box.show() self._connect_signals() def _deactivate(self): """ Reset the title editor interface to its default look """ self.infobar.show() self.textarea.hide() self.editing_box.hide() self._disconnect_signals() def _updateFromSource(self): if self.source is not None: source_text = self.source.get_text() self.log("Title text set to %s", source_text) if source_text is None: # FIXME: sometimes we get a TextOverlay/TitleSource # without a valid text property. This should not happen. source_text = "" self.warning('Source did not have a text property, setting it to "" to avoid pango choking up on None') self.pangobuffer.set_text(source_text) self.textbuffer.set_text(source_text) self.settings['xpos'].set_value(self.source.get_xpos()) self.settings['ypos'].set_value(self.source.get_ypos()) self.settings['valignment'].set_active_id(self.source.get_valignment().value_name) self.settings['halignment'].set_active_id(self.source.get_halignment().value_name) if hasattr(self.source, "get_background"): self.bt["back_color"].set_visible(True) color = self.source.get_background() color = Gdk.RGBA(color / 256 ** 2 % 256 / 255., color / 256 ** 1 % 256 / 255., color / 256 ** 0 % 256 / 255., color / 256 ** 3 % 256 / 255.) self.bt["back_color"].set_rgba(color) else: self.bt["back_color"].set_visible(False) def _updateSourceText(self, updated_obj): if self.source is not None: if self.markup_button.get_active(): text = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) else: text = self.pangobuffer.get_text() self.log("Source text updated to %s", text) self.source.set_text(text) self.seeker.flush() def _updateSource(self, updated_obj): """ Handle changes in one of the advanced property widgets at the bottom """ if self.source is None: return for name, obj in self.settings.items(): if obj == updated_obj: if name == "valignment": self.source.set_valignment(getattr(GES.TextVAlign, obj.get_active_id().upper())) self.settings["ypos"].set_visible(obj.get_active_id() == "position") elif name == "halignment": self.source.set_halignment(getattr(GES.TextHAlign, obj.get_active_id().upper())) self.settings["xpos"].set_visible(obj.get_active_id() == "position") elif name == "xpos": self.settings["halignment"].set_active_id("position") self.source.set_xpos(obj.get_value()) elif name == "ypos": self.settings["valignment"].set_active_id("position") self.source.set_ypos(obj.get_value()) self.seeker.flush() return def _reset(self): #TODO: reset not only text self.markup_button.set_active(False) self.pangobuffer.set_text("") self.textbuffer.set_text("") #Set right buffer self._markupToggleCb(self.markup_button) def set_source(self, source, created=False): # FIXME: this "created" boolean param is a hack """ Set the GESTitleClip to be used with the title editor. This can be called either from the title editor in _createCb, or by track.py to set the source to None. """ self.debug("Source set to %s", str(source)) self.source = source self._reset() self.created = created # We can't just assert source is not None... because track.py may ask us # to reset the source to None if source is None: self._deactivate() else: assert isinstance(source, GES.TextOverlay) or \ isinstance(source, GES.TitleSource) or \ isinstance(source, GES.TitleClip) self._updateFromSource() self._activate() def _createCb(self, unused_button): """ The user clicked the "Create and insert" button, initialize the UI """ source = GES.TitleClip() source.set_text("") source.set_duration(long(Gst.SECOND * 5)) self.set_source(source, True) # TODO: insert on the current layer at the playhead position. # If no space is available, create a new layer to insert to on top. self.app.gui.timeline_ui.insertEnd([self.source]) self.app.gui.timeline_ui.timeline.selection.setToObj(self.source, SELECT) #After insertion consider as not created self.created = False def _connect_signals(self): if not self._signals_connected: self.app.gui.viewer.target.connect("motion-notify-event", self.drag_notify_event) self.app.gui.viewer.target.connect("button-press-event", self.drag_press_event) self.app.gui.viewer.target.connect("button-release-event", self.drag_release_event) self._signals_connected = True def _disconnect_signals(self): if not self._signals_connected: return self.app.gui.viewer.target.disconnect_by_func(self.drag_notify_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_press_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_release_event) self._signals_connected = False def drag_press_event(self, widget, event): if event.button == 1: self._drag_events = [(event.x, event.y)] #Update drag by drag event change, but not too often self.timeout = GLib.timeout_add(100, self.drag_update_event) #If drag goes out for 0.3 second, and do not come back, consider drag end self._drag_updated = True self.timeout = GLib.timeout_add(1000, self.drag_possible_end_event) def drag_possible_end_event(self): if self._drag_updated: #Updated during last timeout, wait more self._drag_updated = False return True else: #Not updated - posibly out of bounds, stop drag self.log("Drag timeout") self._drag_events = [] return False def drag_update_event(self): if len(self._drag_events) > 0: st = self._drag_events[0] self._drag_events = [self._drag_events[-1]] e = self._drag_events[0] xdiff = e[0] - st[0] ydiff = e[1] - st[1] xdiff /= self.app.gui.viewer.target.get_allocated_width() ydiff /= self.app.gui.viewer.target.get_allocated_height() newxpos = self.settings["xpos"].get_value() + xdiff newypos = self.settings["ypos"].get_value() + ydiff self.settings["xpos"].set_value(newxpos) self.settings["ypos"].set_value(newypos) self.seeker.flush() return True else: return False def drag_notify_event(self, widget, event): if len(self._drag_events) > 0 and event.get_state() & Gdk.ModifierType.BUTTON1_MASK: self._drag_updated = True self._drag_events.append((event.x, event.y)) st = self._drag_events[0] e = self._drag_events[-1] def drag_release_event(self, widget, event): self._drag_events = [] def tab_switched(self, unused_notebook, arg1, arg2): if arg2 == 2: self._connect_signals() else: self._disconnect_signals()
class ScaleRuler(gtk.DrawingArea, Zoomable, Loggable): __gsignals__ = { "expose-event": "override", "button-press-event": "override", "button-release-event": "override", "motion-notify-event": "override", "scroll-event": "override", "seek": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_UINT64]) } border = 0 min_tick_spacing = 3 scale = [0, 0, 0, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 3600] subdivide = ((1, 1.0), (2, 0.5), (10, .25)) def __init__(self, instance, hadj): gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") self.app = instance self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.need_update = True self.min_frame_spacing = 5.0 self.frame_height = 5.0 self.frame_rate = gst.Fraction(1 / 1) def _hadjValueChangedCb(self, hadj): self.pixbuf_offset = self.hadj.get_value() self.queue_draw() ## Zoomable interface override def zoomChanged(self): self.need_update = True self.queue_draw() ## timeline position changed method def timelinePositionChanged(self, value, unused_frame=None): self.position = value self.queue_draw() ## gtk.Widget overrides def do_expose_event(self, event): self.log("exposing ScaleRuler %s", list(event.area)) x, y, width, height = event.area self.repaintIfNeeded(width, height) # offset in pixbuf to paint offset_to_paint = self.pixbuf_offset - self.pixbuf_offset_painted self.window.draw_pixbuf( self.style.fg_gc[gtk.STATE_NORMAL], self.pixbuf, int(offset_to_paint), 0, x, y, width, height, gtk.gdk.RGB_DITHER_NONE) # draw the position context = self.window.cairo_create() self.drawPosition(context) return False def do_button_press_event(self, event): self.debug("button pressed at x:%d", event.x) self.pressed = True position = self.pixelToNs(event.x + self.pixbuf_offset) self._seeker.seek(position) return True def do_button_release_event(self, event): self.debug("button released at x:%d", event.x) self.pressed = False # The distinction between the ruler and timeline canvas is theoretical. # If the user interacts with the ruler, have the timeline steal focus # from other widgets. This reactivates keyboard shortcuts for playback. timeline = self.app.gui.timeline_ui timeline._canvas.grab_focus(timeline._root_item) return False def do_motion_notify_event(self, event): position = self.pixelToNs(event.x + self.pixbuf_offset) if self.pressed: self.debug("motion at event.x %d", event.x) self._seeker.seek(position) else: human_time = beautify_length(position) cur_frame = int(position / self.ns_per_frame) + 1 self.set_tooltip_text(human_time + "\n" + _("Frame #%d" % cur_frame)) return False def do_scroll_event(self, event): if event.state & gtk.gdk.CONTROL_MASK: # Control + scroll = zoom if event.direction == gtk.gdk.SCROLL_UP: Zoomable.zoomIn() self.app.gui.timeline_ui.zoomed_fitted = False elif event.direction == gtk.gdk.SCROLL_DOWN: Zoomable.zoomOut() self.app.gui.timeline_ui.zoomed_fitted = False else: # No modifier key held down, just scroll if event.direction == gtk.gdk.SCROLL_UP or\ event.direction == gtk.gdk.SCROLL_LEFT: self.app.gui.timeline_ui.scroll_left() elif event.direction == gtk.gdk.SCROLL_DOWN or\ event.direction == gtk.gdk.SCROLL_RIGHT: self.app.gui.timeline_ui.scroll_right() ## Drawing methods def repaintIfNeeded(self, width, height): """ (re)create the buffered drawable for the Widget """ if self.pixbuf: # The new offset starts before painted in pixbuf if self.pixbuf_offset < self.pixbuf_offset_painted: self.need_update = True # The new offsets end after pixbuf we have if self.pixbuf_offset + width > self.pixbuf_offset_painted + self.pixbuf.get_width(): self.need_update = True else: self.need_update = True # We want to benefit from double-buffering (so as not to recreate the # ruler graphics all the time) yet we don't want to allocate insanely # big pixbufs (which would result in big memory usage, or even not being # able to allocate such a big pixbuf). # # We therefore create a pixbuf with a width of 4 times the max viewable # width (allocation.width) if self.need_update: self.log("Repainting the ruler") if self.pixbuf: del self.pixbuf surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width * self.pixbuf_multiples, height) self.pixbuf_offset_painted = self.pixbuf_offset cr = cairo.Context(surface) self.drawBackground(cr) self.drawRuler(cr) cr = None self.pixbuf = gtk.gdk.pixbuf_new_from_data(surface.get_data(), gtk.gdk.COLORSPACE_RGB, True, 8, surface.get_width(), surface.get_height(), 4 * surface.get_width()) surface = None self.need_update = False def setProjectFrameRate(self, rate): """ Set the lowest scale based on project framerate """ self.frame_rate = rate self.ns_per_frame = float(1 / self.frame_rate) * gst.SECOND self.scale[0] = float(2 / rate) self.scale[1] = float(5 / rate) self.scale[2] = float(10 / rate) def drawBackground(self, cr): setCairoColor(cr, self.style.bg[gtk.STATE_NORMAL]) cr.rectangle(0, 0, cr.get_target().get_width(), cr.get_target().get_height()) cr.fill() offset = int(self.nsToPixel(gst.CLOCK_TIME_NONE)) - self.pixbuf_offset if offset > 0: setCairoColor(cr, self.style.bg[gtk.STATE_ACTIVE]) cr.rectangle(0, 0, int(offset), cr.get_target().get_height()) cr.fill() def drawRuler(self, cr): cr.set_font_face(cairo.ToyFontFace("Cantarell")) cr.set_font_size(15) textwidth = cr.text_extents(time_to_string(0))[2] for scale in self.scale: spacing = Zoomable.zoomratio * scale if spacing >= textwidth * 1.5: break offset = self.pixbuf_offset % spacing zoomRatio = self.zoomratio self.drawFrameBoundaries(cr) self.drawTicks(cr, offset, spacing, scale) self.drawTimes(cr, offset, spacing, scale) def drawTick(self, cr, paintpos, height): # We need to use 0.5 pixel offsets to get a sharp 1 px line in cairo paintpos = int(paintpos - 0.5) + 0.5 height = int(cr.get_target().get_height() * (1 - height)) setCairoColor(cr, self.style.fg[gtk.STATE_NORMAL]) cr.set_line_width(1) cr.move_to(paintpos, height) cr.line_to(paintpos, cr.get_target().get_height()) cr.close_path() cr.stroke() def drawTicks(self, cr, offset, spacing, scale): for subdivide, height in self.subdivide: spc = spacing / float(subdivide) if spc < self.min_tick_spacing: break paintpos = -spacing + 0.5 paintpos += spacing - offset while paintpos < cr.get_target().get_width(): self.drawTick(cr, paintpos, height) paintpos += spc def drawTimes(self, cr, offset, spacing, scale): # figure out what the optimal offset is interval = long(gst.SECOND * scale) seconds = self.pixelToNs(self.pixbuf_offset) paintpos = float(self.border) + 2 if offset > 0: seconds = seconds - (seconds % interval) + interval paintpos += spacing - offset while paintpos < cr.get_target().get_width(): if paintpos < self.nsToPixel(gst.CLOCK_TIME_NONE): state = gtk.STATE_ACTIVE else: state = gtk.STATE_NORMAL timevalue = time_to_string(long(seconds)) setCairoColor(cr, self.style.fg[state]) x_bearing, y_bearing = cr.text_extents("0")[:2] cr.move_to(int(paintpos), 1 - y_bearing) cr.show_text(timevalue) paintpos += spacing seconds += interval def drawFrameBoundaries(self, cr): frame_width = self.nsToPixel(self.ns_per_frame) if frame_width >= self.min_frame_spacing: offset = self.pixbuf_offset % frame_width paintpos = -frame_width + 0.5 height = cr.get_target().get_height() y = int(height - self.frame_height) states = [gtk.STATE_ACTIVE, gtk.STATE_PRELIGHT] paintpos += frame_width - offset frame_num = int(paintpos // frame_width) % 2 while paintpos < cr.get_target().get_width(): setCairoColor(cr, self.style.bg[states[frame_num]]) cr.rectangle(paintpos, y, frame_width, height) cr.fill() frame_num = (frame_num + 1) % 2 paintpos += frame_width def drawPosition(self, context): # a simple RED line will do for now xpos = self.nsToPixel(self.position) + self.border - self.pixbuf_offset context.save() context.set_line_width(1.5) context.set_source_rgb(1.0, 0, 0) context.move_to(xpos, 0) context.line_to(xpos, context.get_target().get_height()) context.stroke() context.restore()
class ViewerWidget(Gtk.DrawingArea, Loggable): """ Widget for displaying properly GStreamer video sink @ivar settings: The settings of the application. @type settings: L{GlobalSettings} """ __gsignals__ = {} def __init__(self, settings=None): Gtk.DrawingArea.__init__(self) Loggable.__init__(self) self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None # FIXME PyGi Styling with Gtk3 #for state in range(Gtk.StateType.INSENSITIVE + 1): #self.modify_bg(state, self.style.black) def init_transformation_events(self): self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK) def show_box(self): if not self.box: self.box = TransformationBox(self.settings) self.box.init_size(self.area) self._update_gradient() self.connect("button-press-event", self.button_press_event) self.connect("button-release-event", self.button_release_event) self.connect("motion-notify-event", self.motion_notify_event) self.connect("size-allocate", self._sizeCb) self.box.set_transformation_properties(self.transformation_properties) self.renderbox() def _sizeCb(self, widget, area): # The transformation box is cleared when using regular rendering # so we need to flush the pipeline self.seeker.flush() def hide_box(self): if self.box: self.box = None self.disconnect_by_func(self.button_press_event) self.disconnect_by_func(self.button_release_event) self.disconnect_by_func(self.motion_notify_event) self.seeker.flush() self.zoom = 1.0 if self.sink: self.sink.set_render_rectangle(*self.area) def set_transformation_properties(self, transformation_properties): self.transformation_properties = transformation_properties def _store_pixbuf(self): """ When not playing, store a pixbuf of the current viewer image. This will allow it to be restored for the transformation box. """ if self.box and self.zoom != 1.0: # The transformation box is active and dezoomed # crop away 1 pixel border to avoid artefacts on the pixbuf self.pixbuf = Gdk.pixbuf_get_from_window(self.get_window(), self.box.area.x + 1, self.box.area.y + 1, self.box.area.width - 2, self.box.area.height - 2) else: self.pixbuf = Gdk.pixbuf_get_from_window(self.get_window(), 0, 0, self.get_window().get_width(), self.get_window().get_height()) self.stored = True def do_realize(self): """ Redefine gtk DrawingArea's do_realize method to handle multiple OSes. This is called when creating the widget to get the window ID. """ Gtk.DrawingArea.do_realize(self) if platform.system() == 'Windows': self.window_xid = self.props.window.handle else: self.window_xid = self.get_property('window').get_xid() def button_release_event(self, widget, event): if event.button == 1: self.box.update_effect_properties() self.box.release_point() self.seeker.flush() self.stored = False return True def button_press_event(self, widget, event): if event.button == 1: self.box.select_point(event) return True def _currentStateCb(self, pipeline, state): self.pipeline = pipeline if state == Gst.State.PAUSED: self._store_pixbuf() self.renderbox() def motion_notify_event(self, widget, event): if event.get_state() & Gdk.ModifierType.BUTTON1_MASK: if self.box.transform(event): if self.stored: self.renderbox() return True def do_expose_event(self, event): self.area = event.area if self.box: self._update_gradient() if self.zoom != 1.0: width = int(float(self.area.width) * self.zoom) height = int(float(self.area.height) * self.zoom) area = ((self.area.width - width) / 2, (self.area.height - height) / 2, width, height) self.sink.set_render_rectangle(*area) else: area = self.area self.box.update_size(area) self.renderbox() def _update_gradient(self): self.gradient_background = cairo.LinearGradient(0, 0, 0, self.area.height) self.gradient_background.add_color_stop_rgb(0.00, .1, .1, .1) self.gradient_background.add_color_stop_rgb(0.50, .2, .2, .2) self.gradient_background.add_color_stop_rgb(1.00, .5, .5, .5) def renderbox(self): if self.box: cr = self.window.cairo_create() cr.push_group() if self.zoom != 1.0: # draw some nice background for zoom out cr.set_source(self.gradient_background) cr.rectangle(0, 0, self.area.width, self.area.height) cr.fill() # translate the drawing of the zoomed out box cr.translate(self.box.area.x, self.box.area.y) # clear the drawingarea with the last known clean video frame # translate when zoomed out if self.pixbuf: if self.box.area.width != self.pixbuf.get_width(): scale = float(self.box.area.width) / float(self.pixbuf.get_width()) cr.save() cr.scale(scale, scale) cr.set_source_pixbuf(self.pixbuf, 0, 0) cr.paint() if self.box.area.width != self.pixbuf.get_width(): cr.restore() if self.pipeline and self.pipeline.get_state()[1] == Gst.State.PAUSED: self.box.draw(cr) cr.pop_group_to_source() cr.paint()
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.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()
class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable): """ Widget for displaying the ruler. Displays a series of consecutive intervals. For each interval its beginning time is shown. If zoomed in enough, shows the frames in alternate colors. """ __gsignals__ = { "button-press-event": "override", "button-release-event": "override", "motion-notify-event": "override", "scroll-event": "override", "seek": (GObject.SignalFlags.RUN_LAST, None, [GObject.TYPE_UINT64]) } def __init__(self, timeline, hadj): Gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") # Allows stealing focus from other GTK widgets, prevent accidents: self.props.can_focus = True self.connect("focus-in-event", self._focusInCb) self.connect("focus-out-event", self._focusOutCb) self.timeline = timeline self._background_color = timeline.get_style_context().lookup_color('theme_bg_color')[1] self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.frame_rate = Gst.Fraction(1 / 1) self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.connect('draw', self.drawCb) self.connect('configure-event', self.configureEventCb) self.callback_id = None self.callback_id_scroll = None self.set_size_request(0, 25) style = self.get_style_context() color_normal = style.get_color(Gtk.StateFlags.NORMAL) color_insensitive = style.get_color(Gtk.StateFlags.INSENSITIVE) self._color_normal = color_normal self._color_dimmed = Gdk.RGBA( *[(x * 3 + y * 2) / 5 for x, y in ((color_normal.red, color_insensitive.red), (color_normal.green, color_insensitive.green), (color_normal.blue, color_insensitive.blue))]) self.scales = SCALES def _focusInCb(self, unused_widget, unused_arg): self.log("Ruler has grabbed focus") self.timeline.setActionsSensitivity(True) def _focusOutCb(self, unused_widget, unused_arg): self.log("Ruler has lost focus") self.timeline.setActionsSensitivity(False) def _hadjValueChangedCb(self, unused_arg): self.pixbuf_offset = self.hadj.get_value() if self.callback_id_scroll is not None: GLib.source_remove(self.callback_id_scroll) self.callback_id_scroll = GLib.timeout_add(100, self._maybeUpdate) ## Zoomable interface override def _maybeUpdate(self): self.queue_draw() self.callback_id = None self.callback_id_scroll = None return False def zoomChanged(self): if self.callback_id is not None: GLib.source_remove(self.callback_id) self.callback_id = GLib.timeout_add(100, self._maybeUpdate) ## timeline position changed method def setPipeline(self, pipeline): pipeline.connect('position', self.timelinePositionCb) def timelinePositionCb(self, unused_pipeline, position): self.position = position self.queue_draw() ## Gtk.Widget overrides def configureEventCb(self, widget, unused_event, unused_data=None): width = widget.get_allocated_width() height = widget.get_allocated_height() self.debug("Configuring, height %d, width %d", width, height) # Destroy previous buffer if self.pixbuf is not None: self.pixbuf.finish() self.pixbuf = None # Create a new buffer self.pixbuf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) return False def drawCb(self, unused_widget, context): if self.pixbuf is None: self.info('No buffer to paint') return False pixbuf = self.pixbuf # Draw on a temporary context and then copy everything. drawing_context = cairo.Context(pixbuf) self.drawBackground(drawing_context) self.drawRuler(drawing_context) self.drawPosition(drawing_context) pixbuf.flush() context.set_source_surface(self.pixbuf, 0.0, 0.0) context.paint() return False def do_button_press_event(self, event): self.debug("button pressed at x:%d", event.x) self.pressed = True position = self.pixelToNs(event.x + self.pixbuf_offset) self._seeker.seek(position, on_idle=True) return True def do_button_release_event(self, event): self.debug("button released at x:%d", event.x) self.grab_focus() # Prevent other widgets from being confused self.pressed = False return False def do_motion_notify_event(self, event): position = self.pixelToNs(event.x + self.pixbuf_offset) if self.pressed: self.debug("motion at event.x %d", event.x) self._seeker.seek(position, on_idle=True) human_time = beautify_length(position) cur_frame = int(position / self.ns_per_frame) + 1 self.set_tooltip_text(human_time + "\n" + _("Frame #%d" % cur_frame)) return False def do_scroll_event(self, event): if event.scroll.state & Gdk.ModifierType.CONTROL_MASK: # Control + scroll = zoom if event.scroll.direction == Gdk.ScrollDirection.UP: Zoomable.zoomIn() self.timeline.zoomed_fitted = False elif event.scroll.direction == Gdk.ScrollDirection.DOWN: Zoomable.zoomOut() self.timeline.zoomed_fitted = False else: # No modifier key held down, just scroll if (event.scroll.direction == Gdk.ScrollDirection.UP or event.scroll.direction == Gdk.ScrollDirection.LEFT): self.timeline.scroll_left() elif (event.scroll.direction == Gdk.ScrollDirection.DOWN or event.scroll.direction == Gdk.ScrollDirection.RIGHT): self.timeline.scroll_right() def setProjectFrameRate(self, rate): """ Set the lowest scale based on project framerate """ self.frame_rate = rate self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.scales = (float(2 / rate), float(5 / rate), float(10 / rate)) + SCALES ## Drawing methods def drawBackground(self, context): style = self.get_style_context() set_cairo_color(context, self._background_color) width = context.get_target().get_width() height = context.get_target().get_height() context.rectangle(0, 0, width, height) context.fill() offset = int(self.nsToPixel(Gst.CLOCK_TIME_NONE)) - self.pixbuf_offset if offset > 0: set_cairo_color(context, style.get_background_color(Gtk.StateFlags.ACTIVE)) context.rectangle(0, 0, int(offset), context.get_target().get_height()) context.fill() def drawRuler(self, context): context.set_font_face(NORMAL_FONT) context.set_font_size(NORMAL_FONT_SIZE) spacing, scale = self._getSpacing(context) offset = self.pixbuf_offset % spacing self.drawFrameBoundaries(context) self.drawTicks(context, offset, spacing) self.drawTimes(context, offset, spacing, scale) def _getSpacing(self, context): textwidth = context.text_extents(time_to_string(0))[2] zoom = Zoomable.zoomratio for scale in self.scales: spacing = scale * zoom if spacing >= textwidth * 1.5: return spacing, scale raise Exception("Failed to find an interval size for textwidth:%s, zoomratio:%s" % (textwidth, Zoomable.zoomratio)) def drawTicks(self, context, offset, spacing): for count_per_interval, height_ratio in TICK_TYPES: space = float(spacing) / count_per_interval if space < MIN_TICK_SPACING_PIXELS: break paintpos = 0.5 - offset set_cairo_color(context, self._color_normal) while paintpos < context.get_target().get_width(): self._drawTick(context, paintpos, height_ratio) paintpos += space def _drawTick(self, context, paintpos, height_ratio): # We need to use 0.5 pixel offsets to get a sharp 1 px line in cairo paintpos = int(paintpos - 0.5) + 0.5 target_height = context.get_target().get_height() y = int(target_height * (1 - height_ratio)) context.set_line_width(1) context.move_to(paintpos, y) context.line_to(paintpos, target_height) context.close_path() context.stroke() def drawTimes(self, context, offset, spacing, scale): # figure out what the optimal offset is interval = long(Gst.SECOND * scale) current_time = self.pixelToNs(self.pixbuf_offset) paintpos = TIMES_LEFT_MARGIN_PIXELS if offset > 0: current_time = current_time - (current_time % interval) + interval paintpos += spacing - offset state = Gtk.StateFlags.NORMAL style = self.get_style_context() set_cairo_color(context, style.get_color(state)) y_bearing = context.text_extents("0")[1] millis = scale < 1 def split(x): # Seven elements: h : mm : ss . mmm # Using negative indices because the first element (hour) # can have a variable length. return x[:-10], x[-10], x[-9:-7], x[-7], x[-6:-4], x[-4], x[-3:] previous = split(time_to_string(max(0, current_time - interval))) width = context.get_target().get_width() while paintpos < width: context.move_to(int(paintpos), 1 - y_bearing) current = split(time_to_string(long(current_time))) self._drawTime(context, current, previous, millis) previous = current paintpos += spacing current_time += interval def _drawTime(self, context, current, previous, millis): hour = int(current[0]) for index, (element, previous_element) in enumerate(zip(current, previous)): if index <= 1 and not hour: continue if index >= 5 and not millis: break if element == previous_element: color = self._color_dimmed else: color = self._color_normal set_cairo_color(context, color) # Display the millis with a smaller font small = index >= 5 if small: context.set_font_size(SMALL_FONT_SIZE) context.show_text(element) if small: context.set_font_size(NORMAL_FONT_SIZE) def drawFrameBoundaries(self, context): """ Draw the alternating rectangles that represent the project frames at high zoom levels. These are based on the framerate set in the project settings, not the actual frames on a video codec level. """ frame_width = self.nsToPixel(self.ns_per_frame) if not frame_width >= FRAME_MIN_WIDTH_PIXELS: return offset = self.pixbuf_offset % frame_width height = context.get_target().get_height() y = int(height - FRAME_HEIGHT_PIXELS) # INSENSITIVE is a dark shade of gray, but lacks contrast # SELECTED will be bright blue and more visible to represent frames style = self.get_style_context() states = [style.get_background_color(Gtk.StateFlags.ACTIVE), style.get_background_color(Gtk.StateFlags.SELECTED)] frame_num = int(self.pixelToNs(self.pixbuf_offset) * float(self.frame_rate) / Gst.SECOND) paintpos = self.pixbuf_offset - offset max_pos = context.get_target().get_width() + self.pixbuf_offset while paintpos < max_pos: paintpos = self.nsToPixel(1 / float(self.frame_rate) * Gst.SECOND * frame_num) set_cairo_color(context, states[(frame_num + 1) % 2]) context.rectangle(0.5 + paintpos - self.pixbuf_offset, y, frame_width, height) context.fill() frame_num += 1 def drawPosition(self, context): # Add 0.5 so that the line center is at the middle of the pixel, # without this the line appears blurry. xpos = self.nsToPixel(self.position) - self.pixbuf_offset + 0.5 context.set_line_width(PLAYHEAD_WIDTH + 2) set_cairo_color(context, PLAYHEAD_COLOR) context.move_to(xpos, 0) context.line_to(xpos, context.get_target().get_height()) context.stroke()
class TitleEditor(Loggable): """ Widget for configuring the selected title. @type app: L{Pitivi} """ def __init__(self, app): Loggable.__init__(self) self.app = app self.action_log = app.action_log self.settings = {} self.source = None self.seeker = Seeker() # Drag attributes self._drag_events = [] self._signals_connected = False self._setting_props = False self._setting_initial_props = False self._children_props_handler = None self._createUI() def _createUI(self): builder = Gtk.Builder() builder.add_from_file(os.path.join(get_ui_dir(), "titleeditor.ui")) builder.connect_signals(self) self.widget = builder.get_object("box1") # To be used by tabsmanager self.infobar = builder.get_object("infobar") self.editing_box = builder.get_object("editing_box") self.textarea = builder.get_object("textview") toolbar = builder.get_object("toolbar") toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR) self.textbuffer = Gtk.TextBuffer() self.textarea.set_buffer(self.textbuffer) self.textbuffer.connect("changed", self._textChangedCb) self.font_button = builder.get_object("fontbutton1") self.foreground_color_button = builder.get_object("fore_text_color") self.background_color_button = builder.get_object("back_color") settings = ["valignment", "halignment", "xpos", "ypos"] for setting in settings: self.settings[setting] = builder.get_object(setting) for n, en in list({_("Custom"): "position", _("Top"): "top", _("Center"): "center", _("Bottom"): "bottom", _("Baseline"): "baseline"}.items()): self.settings["valignment"].append(en, n) for n, en in list({_("Custom"): "position", _("Left"): "left", _("Center"): "center", _("Right"): "right"}.items()): self.settings["halignment"].append(en, n) self._deactivate() def _setChildProperty(self, name, value): self.action_log.begin("Title %s change" % name) self._setting_props = True self.source.set_child_property(name, value) self._setting_props = False self.action_log.commit() def _backgroundColorButtonCb(self, widget): color = gdk_rgba_to_argb(widget.get_rgba()) self.debug("Setting title background color to %x", color) self._setChildProperty("foreground-color", color) def _frontTextColorButtonCb(self, widget): color = gdk_rgba_to_argb(widget.get_rgba()) self.debug("Setting title foreground color to %x", color) # TODO: Use set_text_color when we work with TitleSources instead of # TitleClips self._setChildProperty("color", color) def _fontButtonCb(self, widget): font_desc = widget.get_font_desc().to_string() self.debug("Setting font desc to %s", font_desc) self._setChildProperty("font-desc", font_desc) def _activate(self): """ Show the title editing UI widgets and hide the infobar """ self.infobar.hide() self.textarea.show() self.editing_box.show() self._connect_signals() def _deactivate(self): """ Reset the title editor interface to its default look """ self.infobar.show() self.textarea.hide() self.editing_box.hide() self._disconnect_signals() def _setWidgetText(self): res, source_text = self.source.get_child_property("text") text = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) if text == source_text: return False if res is False: # FIXME: sometimes we get a TextOverlay/TitleSource # without a valid text property. This should not happen. source_text = "" self.warning( 'Source did not have a text property, setting it to "" to avoid pango choking up on None') self.log("Title text set to %s", source_text) self.textbuffer.set_text(source_text) return True def _updateFromSource(self): if not self.source: # Nothing to update from. return self._setWidgetText() self.settings['xpos'].set_value(self.source.get_child_property("xpos")[1]) self.settings['ypos'].set_value(self.source.get_child_property("ypos")[1]) self.settings['valignment'].set_active_id( self.source.get_child_property("valignment")[1].value_name) self.settings['halignment'].set_active_id( self.source.get_child_property("halignment")[1].value_name) font_desc = Pango.FontDescription.from_string( self.source.get_child_property("font-desc")[1]) self.font_button.set_font_desc(font_desc) color = argb_to_gdk_rgba(self.source.get_child_property("color")[1]) self.foreground_color_button.set_rgba(color) color = argb_to_gdk_rgba(self.source.get_child_property("foreground-color")[1]) self.background_color_button.set_rgba(color) def _textChangedCb(self, unused_updated_obj): if not self.source: # Nothing to update. return text = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) self.log("Source text updated to %s", text) self._setChildProperty("text", text) def _updateSource(self, updated_obj): """ Handle changes in one of the advanced property widgets at the bottom """ if not self.source: # Nothing to update. return for name, obj in list(self.settings.items()): if obj == updated_obj: if name == "valignment": value = getattr(GES.TextVAlign, obj.get_active_id().upper()) visible = obj.get_active_id() == "position" self.settings["ypos"].set_visible(visible) elif name == "halignment": value = getattr(GES.TextHAlign, obj.get_active_id().upper()) visible = obj.get_active_id() == "position" self.settings["xpos"].set_visible(visible) else: value = obj.get_value() self._setChildProperty(name, value) return def set_source(self, source): """ Set the clip to be edited with this editor. @type source: L{GES.TitleSource} """ self.debug("Source set to %s", source) self._deactivate() assert isinstance(source, GES.TextOverlay) or \ isinstance(source, GES.TitleSource) self.source = source self._updateFromSource() self._activate() def unset_source(self): self._deactivate() self.source = None def _createCb(self, unused_button): """ The user clicked the "Create and insert" button, initialize the UI """ clip = GES.TitleClip() clip.set_text("") clip.set_duration(int(Gst.SECOND * 5)) # TODO: insert on the current layer at the playhead position. # If no space is available, create a new layer to insert to on top. self.app.gui.timeline_ui.insertEnd([clip]) self.app.gui.timeline_ui.timeline.selection.setToObj(clip, SELECT) self._setting_initial_props = True clip.set_color(FOREGROUND_DEFAULT_COLOR) clip.set_background(BACKGROUND_DEFAULT_COLOR) self._setting_initial_props = False def _propertyChangedCb(self, source, unused_gstelement, pspec): if self._setting_initial_props: return if self._setting_props: self.seeker.flush() return flush = False if pspec.name == "text": if self._setWidgetText() is True: flush = True elif pspec.name in ["xpos", "ypos"]: value = self.source.get_child_property(pspec.name)[1] if self.settings[pspec.name].get_value() == value: return flush = True self.settings[pspec.name].set_value(value) elif pspec.name in ["valignment", "halignment"]: value = self.source.get_child_property(pspec.name)[1].value_name if self.settings[pspec.name].get_active_id() == value: return flush = True self.settings[pspec.name].set_active_id(value) elif pspec.name == "font-desc": value = self.source.get_child_property("font-desc")[1] if self.font_button.get_font_desc() == value: return flush = True font_desc = Pango.FontDescription.from_string(value) self.font_button.set_font_desc(font_desc) elif pspec.name == "color": color = argb_to_gdk_rgba(self.source.get_child_property("color")[1]) if color == self.foreground_color_button.get_rgba(): return flush = True self.foreground_color_button.set_rgba(color) elif pspec.name == "foreground-color": color = argb_to_gdk_rgba(self.source.get_child_property("foreground-color")[1]) if color == self.background_color_button.get_rgba(): return flush = True self.background_color_button.set_rgba(color) if flush is True: self.seeker.flush() def _connect_signals(self): if self.source and not self._children_props_handler: self._children_props_handler = self.source.connect('deep-notify', self._propertyChangedCb) if not self._signals_connected: self.app.gui.viewer.target.connect( "motion-notify-event", self.drag_notify_event) self.app.gui.viewer.target.connect( "button-press-event", self.drag_press_event) self.app.gui.viewer.target.connect( "button-release-event", self.drag_release_event) self._signals_connected = True def _disconnect_signals(self): if self._children_props_handler is not None: self.source.disconnect(self._children_props_handler) self._children_props_handler = None if not self._signals_connected: return self.app.gui.viewer.target.disconnect_by_func(self.drag_notify_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_press_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_release_event) self._signals_connected = False def drag_press_event(self, unused_widget, event): if event.button == 1: self._drag_events = [(event.x, event.y)] # Update drag by drag event change, but not too often self.timeout = GLib.timeout_add(100, self.drag_update_event) # If drag goes out for 0.3 second, and do not come back, consider # drag end self._drag_updated = True self.timeout = GLib.timeout_add(1000, self.drag_possible_end_event) def drag_possible_end_event(self): if self._drag_updated: # Updated during last timeout, wait more self._drag_updated = False return True else: # Not updated - posibly out of bounds, stop drag self.log("Drag timeout") self._drag_events = [] return False def drag_update_event(self): if len(self._drag_events) > 0: st = self._drag_events[0] self._drag_events = [self._drag_events[-1]] e = self._drag_events[0] xdiff = e[0] - st[0] ydiff = e[1] - st[1] xdiff /= self.app.gui.viewer.target.get_allocated_width() ydiff /= self.app.gui.viewer.target.get_allocated_height() newxpos = self.settings["xpos"].get_value() + xdiff newypos = self.settings["ypos"].get_value() + ydiff self.settings["xpos"].set_value(newxpos) self.settings["ypos"].set_value(newypos) return True else: return False def drag_notify_event(self, unused_widget, event): if len(self._drag_events) > 0 and event.get_state() & Gdk.ModifierType.BUTTON1_MASK: self._drag_updated = True self._drag_events.append((event.x, event.y)) st = self._drag_events[0] e = self._drag_events[-1] def drag_release_event(self, unused_widget, unused_event): self._drag_events = [] def tabSwitchedCb(self, unused_notebook, page_widget, unused_page_index): if self.widget == page_widget: self._connect_signals() else: self._disconnect_signals() def selectionChangedCb(self, selection): selected_clip = selection.getSingleClip(GES.TitleClip) source = None if selected_clip: source = selected_clip.get_children(False)[0] if source: self.set_source(source) else: self.unset_source()
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("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.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 ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable): __gsignals__ = { "button-press-event": "override", "button-release-event": "override", "motion-notify-event": "override", "scroll-event": "override", "seek": (GObject.SignalFlags.RUN_LAST, None, [GObject.TYPE_UINT64]) } border = 0 min_tick_spacing = 3 scale = [0, 0, 0, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 3600] subdivide = ((1, 1.0), (2, 0.5), (10, .25)) def __init__(self, instance, hadj): Gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") self.app = instance self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.min_frame_spacing = 5.0 self.frame_height = 5.0 self.frame_rate = Gst.Fraction(1 / 1) self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.connect('draw', self.drawCb) self.connect('configure-event', self.configureEventCb) self.callback_id = None self.callback_id_scroll = None def _hadjValueChangedCb(self, hadj): self.pixbuf_offset = self.hadj.get_value() if self.callback_id_scroll is not None: GLib.source_remove(self.callback_id_scroll) self.callback_id_scroll = GLib.timeout_add(100, self._maybeUpdate) ## Zoomable interface override def _maybeUpdate(self): self.queue_draw() self.callback_id = None return False def zoomChanged(self): if self.callback_id is not None: GLib.source_remove(self.callback_id) self.callback_id = GLib.timeout_add(100, self._maybeUpdate) ## timeline position changed method def timelinePositionChanged(self, value, unused_frame=None): self.position = value self.queue_draw() ## Gtk.Widget overrides def configureEventCb(self, widget, event, data=None): self.debug("Configuring, height %d, width %d", widget.get_allocated_width(), widget.get_allocated_height()) # Destroy previous buffer if self.pixbuf is not None: self.pixbuf.finish() self.pixbuf = None # Create a new buffer self.pixbuf = cairo.ImageSurface(cairo.FORMAT_ARGB32, widget.get_allocated_width(), widget.get_allocated_height()) return False def drawCb(self, widget, cr): if self.pixbuf is not None: db = self.pixbuf # Create cairo context with double buffer as is DESTINATION cc = cairo.Context(db) #draw everything self.drawBackground(cc) self.drawRuler(cc) self.drawPosition(cc) db.flush() cr.set_source_surface(self.pixbuf, 0.0, 0.0) cr.paint() else: self.info('No buffer to paint buffer') return False def do_button_press_event(self, event): self.debug("button pressed at x:%d", event.x) self.pressed = True position = self.pixelToNs(event.x + self.pixbuf_offset) self._seeker.seek(position) return True def do_button_release_event(self, event): self.debug("button released at x:%d", event.x) self.pressed = False return False def do_motion_notify_event(self, event): position = self.pixelToNs(event.x + self.pixbuf_offset) if self.pressed: self.debug("motion at event.x %d", event.x) self._seeker.seek(position) human_time = beautify_length(position) cur_frame = int(position / self.ns_per_frame) + 1 self.set_tooltip_text(human_time + "\n" + _("Frame #%d" % cur_frame)) return False def do_scroll_event(self, event): if event.scroll.state & Gdk.ModifierType.CONTROL_MASK: # Control + scroll = zoom if event.scroll.direction == Gdk.ScrollDirection.UP: Zoomable.zoomIn() self.app.gui.timeline_ui.zoomed_fitted = False elif event.scroll.direction == Gdk.ScrollDirection.DOWN: Zoomable.zoomOut() self.app.gui.timeline_ui.zoomed_fitted = False else: # No modifier key held down, just scroll if (event.scroll.direction == Gdk.ScrollDirection.UP or event.scroll.direction == Gdk.ScrollDirection.LEFT): self.app.gui.timeline_ui.scroll_left() elif (event.scroll.direction == Gdk.ScrollDirection.DOWN or event.scroll.direction == Gdk.ScrollDirection.RIGHT): self.app.gui.timeline_ui.scroll_right() def setProjectFrameRate(self, rate): """ Set the lowest scale based on project framerate """ self.frame_rate = rate self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.scale[0] = float(2 / rate) self.scale[1] = float(5 / rate) self.scale[2] = float(10 / rate) ## Drawing methods def drawBackground(self, cr): style = self.get_style_context() setCairoColor(cr, style.get_background_color(Gtk.StateFlags.NORMAL)) cr.rectangle(0, 0, cr.get_target().get_width(), cr.get_target().get_height()) cr.fill() offset = int(self.nsToPixel(Gst.CLOCK_TIME_NONE)) - self.pixbuf_offset if offset > 0: setCairoColor(cr, style.get_background_color(Gtk.StateFlags.ACTIVE)) cr.rectangle(0, 0, int(offset), cr.get_target().get_height()) cr.fill() def drawRuler(self, cr): # FIXME use system defaults cr.set_font_face(cairo.ToyFontFace("Cantarell")) cr.set_font_size(13) textwidth = cr.text_extents(time_to_string(0))[2] for scale in self.scale: spacing = Zoomable.zoomratio * scale if spacing >= textwidth * 1.5: break offset = self.pixbuf_offset % spacing self.drawFrameBoundaries(cr) self.drawTicks(cr, offset, spacing, scale) self.drawTimes(cr, offset, spacing, scale) def drawTick(self, cr, paintpos, height): # We need to use 0.5 pixel offsets to get a sharp 1 px line in cairo paintpos = int(paintpos - 0.5) + 0.5 height = int(cr.get_target().get_height() * (1 - height)) style = self.get_style_context() setCairoColor(cr, style.get_color(Gtk.StateType.NORMAL)) cr.set_line_width(1) cr.move_to(paintpos, height) cr.line_to(paintpos, cr.get_target().get_height()) cr.close_path() cr.stroke() def drawTicks(self, cr, offset, spacing, scale): for subdivide, height in self.subdivide: spc = spacing / float(subdivide) if spc < self.min_tick_spacing: break paintpos = -spacing + 0.5 paintpos += spacing - offset while paintpos < cr.get_target().get_width(): self.drawTick(cr, paintpos, height) paintpos += spc def drawTimes(self, cr, offset, spacing, scale): # figure out what the optimal offset is interval = long(Gst.SECOND * scale) seconds = self.pixelToNs(self.pixbuf_offset) paintpos = float(self.border) + 2 if offset > 0: seconds = seconds - (seconds % interval) + interval paintpos += spacing - offset while paintpos < cr.get_target().get_width(): if paintpos < self.nsToPixel(Gst.CLOCK_TIME_NONE): state = Gtk.StateType.ACTIVE else: state = Gtk.StateType.NORMAL timevalue = time_to_string(long(seconds)) style = self.get_style_context() setCairoColor(cr, style.get_color(state)) x_bearing, y_bearing = cr.text_extents("0")[:2] cr.move_to(int(paintpos), 1 - y_bearing) cr.show_text(timevalue) paintpos += spacing seconds += interval def drawFrameBoundaries(self, cr): """ Draw the alternating rectangles that represent the project frames at high zoom levels. These are based on the framerate set in the project settings, not the actual frames on a video codec level. """ frame_width = self.nsToPixel(self.ns_per_frame) if not frame_width >= self.min_frame_spacing: return offset = self.pixbuf_offset % frame_width height = cr.get_target().get_height() y = int(height - self.frame_height) # INSENSITIVE is a dark shade of gray, but lacks contrast # SELECTED will be bright blue and more visible to represent frames style = self.get_style_context() states = [style.get_background_color(Gtk.StateFlags.ACTIVE), style.get_background_color(Gtk.StateFlags.SELECTED)] frame_num = int(self.pixelToNs(self.pixbuf_offset) * float(self.frame_rate) / Gst.SECOND) paintpos = self.pixbuf_offset - offset max_pos = cr.get_target().get_width() + self.pixbuf_offset while paintpos < max_pos: paintpos = self.nsToPixel(1 / float(self.frame_rate) * Gst.SECOND * frame_num) setCairoColor(cr, states[(frame_num + 1) % 2]) cr.rectangle(0.5 + paintpos - self.pixbuf_offset, y, frame_width, height) cr.fill() frame_num += 1 def drawPosition(self, context): # a simple RED line will do for now xpos = self.nsToPixel(self.position) + self.border - self.pixbuf_offset context.save() context.set_line_width(1.5) context.set_source_rgb(1.0, 0, 0) context.move_to(xpos, 0) context.line_to(xpos, context.get_target().get_height()) context.stroke() context.restore()
class TitleEditor(Loggable): def __init__(self, instance, uimap): Loggable.__init__(self) Signallable.__init__(self) self.app = instance self.bt = {} self.settings = {} self.source = None self.created = False self.seeker = Seeker() #Drag attributes self._drag_events = [] self._signals_connected = False self._createUI() self.textbuffer = Gtk.TextBuffer() self.pangobuffer = InteractivePangoBuffer() self.textarea.set_buffer(self.pangobuffer) self.textbuffer.connect("changed", self._updateSourceText) self.pangobuffer.connect("changed", self._updateSourceText) #Connect buttons self.pangobuffer.setup_widget_from_pango(self.bt["bold"], "<b>bold</b>") self.pangobuffer.setup_widget_from_pango(self.bt["italic"], "<i>italic</i>") def _createUI(self): builder = Gtk.Builder() builder.add_from_file(os.path.join(get_ui_dir(), "titleeditor.ui")) builder.connect_signals(self) self.widget = builder.get_object("box1") # To be used by tabsmanager self.infobar = builder.get_object("infobar") self.editing_box = builder.get_object("editing_box") self.textarea = builder.get_object("textview") self.markup_button = builder.get_object("markupToggle") toolbar = builder.get_object("toolbar") toolbar.get_style_context().add_class("inline-toolbar") buttons = ["bold", "italic", "font", "font_fore_color", "back_color"] for button in buttons: self.bt[button] = builder.get_object(button) settings = ["valignment", "halignment", "xpos", "ypos"] for setting in settings: self.settings[setting] = builder.get_object(setting) for n, en in { _("Custom"): "position", _("Top"): "top", _("Center"): "center", _("Bottom"): "bottom", _("Baseline"): "baseline" }.items(): self.settings["valignment"].append(en, n) for n, en in { _("Custom"): "position", _("Left"): "left", _("Center"): "center", _("Right"): "right" }.items(): self.settings["halignment"].append(en, n) self._deactivate() def _textviewFocusedCb(self, unused_widget, unused_event): self.app.gui.setActionsSensitive(False) def _textviewUnfocusedCb(self, unused_widget, unused_event): self.app.gui.setActionsSensitive(True) def _backgroundColorButtonCb(self, widget): self.textarea.modify_base(self.textarea.get_state(), widget.get_color()) color = widget.get_rgba() color_int = 0 color_int += int(color.red * 255) * 256**2 color_int += int(color.green * 255) * 256**1 color_int += int(color.blue * 255) * 256**0 color_int += int(color.alpha * 255) * 256**3 self.debug("Setting title background color to %s", hex(color_int)) self.source.set_background(color_int) def _frontTextColorButtonCb(self, widget): suc, a, t, s = Pango.parse_markup( "<span color='" + widget.get_color().to_string() + "'>color</span>", -1, u'\x00') ai = a.get_iterator() font, lang, attrs = ai.get_font() tags = self.pangobuffer.get_tags_from_attrs(None, None, attrs) self.pangobuffer.apply_tag_to_selection(tags[0]) def _fontButtonCb(self, widget): font_desc = widget.get_font_name().split(" ") font_face = " ".join(font_desc[:-1]) font_size = str(int(font_desc[-1]) * 1024) text = "<span face='" + font_face + "'><span size='" + font_size + "'>text</span></span>" suc, a, t, s = Pango.parse_markup(text, -1, u'\x00') ai = a.get_iterator() font, lang, attrs = ai.get_font() tags = self.pangobuffer.get_tags_from_attrs(font, None, attrs) for tag in tags: self.pangobuffer.apply_tag_to_selection(tag) def _markupToggleCb(self, markup_button): # FIXME: either make this feature rock-solid or replace it by a # Clear markup" button. Currently it is possible for the user to create # invalid markup (causing errors) or to get our textbuffer confused self.textbuffer.disconnect_by_func(self._updateSourceText) self.pangobuffer.disconnect_by_func(self._updateSourceText) if markup_button.get_active(): self.textbuffer.set_text(self.pangobuffer.get_text()) self.textarea.set_buffer(self.textbuffer) for name in self.bt: self.bt[name].set_sensitive(False) else: txt = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) self.pangobuffer.set_text(txt) self.textarea.set_buffer(self.pangobuffer) for name in self.bt: self.bt[name].set_sensitive(True) self.textbuffer.connect("changed", self._updateSourceText) self.pangobuffer.connect("changed", self._updateSourceText) def _activate(self): """ Show the title editing UI widgets and hide the infobar """ self.infobar.hide() self.textarea.show() self.editing_box.show() self._connect_signals() def _deactivate(self): """ Reset the title editor interface to its default look """ self.infobar.show() self.textarea.hide() self.editing_box.hide() self._disconnect_signals() def _updateFromSource(self): if self.source is not None: source_text = self.source.get_text() self.log("Title text set to %s", source_text) if source_text is None: # FIXME: sometimes we get a TextOverlay/TitleSource # without a valid text property. This should not happen. source_text = "" self.warning( 'Source did not have a text property, setting it to "" to avoid pango choking up on None' ) self.pangobuffer.set_text(source_text) self.textbuffer.set_text(source_text) self.settings['xpos'].set_value(self.source.get_xpos()) self.settings['ypos'].set_value(self.source.get_ypos()) self.settings['valignment'].set_active_id( self.source.get_valignment().value_name) self.settings['halignment'].set_active_id( self.source.get_halignment().value_name) if hasattr(self.source, "get_background"): self.bt["back_color"].set_visible(True) color = self.source.get_background() color = Gdk.RGBA(color / 256**2 % 256 / 255., color / 256**1 % 256 / 255., color / 256**0 % 256 / 255., color / 256**3 % 256 / 255.) self.bt["back_color"].set_rgba(color) else: self.bt["back_color"].set_visible(False) def _updateSourceText(self, updated_obj): if self.source is not None: if self.markup_button.get_active(): text = self.textbuffer.get_text( self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) else: text = self.pangobuffer.get_text() self.log("Source text updated to %s", text) self.source.set_text(text) self.seeker.flush() def _updateSource(self, updated_obj): """ Handle changes in one of the advanced property widgets at the bottom """ if self.source is None: return for name, obj in self.settings.items(): if obj == updated_obj: if name == "valignment": self.source.set_valignment( getattr(GES.TextVAlign, obj.get_active_id().upper())) self.settings["ypos"].set_visible( obj.get_active_id() == "position") elif name == "halignment": self.source.set_halignment( getattr(GES.TextHAlign, obj.get_active_id().upper())) self.settings["xpos"].set_visible( obj.get_active_id() == "position") elif name == "xpos": self.settings["halignment"].set_active_id("position") self.source.set_xpos(obj.get_value()) elif name == "ypos": self.settings["valignment"].set_active_id("position") self.source.set_ypos(obj.get_value()) self.seeker.flush() return def _reset(self): #TODO: reset not only text self.markup_button.set_active(False) self.pangobuffer.set_text("") self.textbuffer.set_text("") #Set right buffer self._markupToggleCb(self.markup_button) def set_source( self, source, created=False): # FIXME: this "created" boolean param is a hack """ Set the GESTitleClip to be used with the title editor. This can be called either from the title editor in _createCb, or by track.py to set the source to None. """ self.debug("Source set to %s", str(source)) self.source = source self._reset() self.created = created # We can't just assert source is not None... because track.py may ask us # to reset the source to None if source is None: self._deactivate() else: assert isinstance(source, GES.TextOverlay) or \ isinstance(source, GES.TitleSource) or \ isinstance(source, GES.TitleClip) self._updateFromSource() self._activate() def _createCb(self, unused_button): """ The user clicked the "Create and insert" button, initialize the UI """ source = GES.TitleClip() source.set_text("") source.set_duration(long(Gst.SECOND * 5)) self.set_source(source, True) # TODO: insert on the current layer at the playhead position. # If no space is available, create a new layer to insert to on top. self.app.gui.timeline_ui.insertEnd([self.source]) self.app.gui.timeline_ui.timeline.selection.setToObj( self.source, SELECT) #After insertion consider as not created self.created = False def _connect_signals(self): if not self._signals_connected: self.app.gui.viewer.target.connect("motion-notify-event", self.drag_notify_event) self.app.gui.viewer.target.connect("button-press-event", self.drag_press_event) self.app.gui.viewer.target.connect("button-release-event", self.drag_release_event) self._signals_connected = True def _disconnect_signals(self): if not self._signals_connected: return self.app.gui.viewer.target.disconnect_by_func(self.drag_notify_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_press_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_release_event) self._signals_connected = False def drag_press_event(self, widget, event): if event.button == 1: self._drag_events = [(event.x, event.y)] #Update drag by drag event change, but not too often self.timeout = GLib.timeout_add(100, self.drag_update_event) #If drag goes out for 0.3 second, and do not come back, consider drag end self._drag_updated = True self.timeout = GLib.timeout_add(1000, self.drag_possible_end_event) def drag_possible_end_event(self): if self._drag_updated: #Updated during last timeout, wait more self._drag_updated = False return True else: #Not updated - posibly out of bounds, stop drag self.log("Drag timeout") self._drag_events = [] return False def drag_update_event(self): if len(self._drag_events) > 0: st = self._drag_events[0] self._drag_events = [self._drag_events[-1]] e = self._drag_events[0] xdiff = e[0] - st[0] ydiff = e[1] - st[1] xdiff /= self.app.gui.viewer.target.get_allocated_width() ydiff /= self.app.gui.viewer.target.get_allocated_height() newxpos = self.settings["xpos"].get_value() + xdiff newypos = self.settings["ypos"].get_value() + ydiff self.settings["xpos"].set_value(newxpos) self.settings["ypos"].set_value(newypos) self.seeker.flush() return True else: return False def drag_notify_event(self, widget, event): if len(self._drag_events ) > 0 and event.get_state() & Gdk.ModifierType.BUTTON1_MASK: self._drag_updated = True self._drag_events.append((event.x, event.y)) st = self._drag_events[0] e = self._drag_events[-1] def drag_release_event(self, widget, event): self._drag_events = [] def tab_switched(self, unused_notebook, arg1, arg2): if arg2 == 2: self._connect_signals() else: self._disconnect_signals()
class ViewerWidget(Gtk.AspectFrame, Loggable): """ Widget for displaying a GStreamer video sink. @ivar settings: The settings of the application. @type settings: L{GlobalSettings} """ __gsignals__ = {} def __init__(self, settings=None, realizedCb=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=1.0, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.drawing_area = Gtk.DrawingArea() self.drawing_area.set_double_buffered(False) self.drawing_area.connect("draw", self._drawCb, None) # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! if realizedCb: self.drawing_area.connect("realize", realizedCb, self) self.add(self.drawing_area) self.drawing_area.show() self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None # FIXME PyGi Styling with Gtk3 # for state in range(Gtk.StateType.INSENSITIVE + 1): # self.modify_bg(state, self.style.black) def _drawCb(self, unused, unused1, unused2): if self.sink: self.sink.expose() def setDisplayAspectRatio(self, ratio): self.set_property("ratio", float(ratio)) def init_transformation_events(self): self.fixme("TransformationBox disabled") """ self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK) """ def show_box(self): self.fixme("TransformationBox disabled") """ if not self.box: self.box = TransformationBox(self.settings) self.box.init_size(self.area) self._update_gradient() self.connect("button-press-event", self.button_press_event) self.connect("button-release-event", self.button_release_event) self.connect("motion-notify-event", self.motion_notify_event) self.connect("size-allocate", self._sizeCb) self.box.set_transformation_properties(self.transformation_properties) self.renderbox() """ def _sizeCb(self, unused_widget, unused_area): # The transformation box is cleared when using regular rendering # so we need to flush the pipeline self.seeker.flush() def hide_box(self): if self.box: self.box = None self.disconnect_by_func(self.button_press_event) self.disconnect_by_func(self.button_release_event) self.disconnect_by_func(self.motion_notify_event) self.seeker.flush() self.zoom = 1.0 if self.sink: self.sink.set_render_rectangle(*self.area) def set_transformation_properties(self, transformation_properties): self.transformation_properties = transformation_properties def _store_pixbuf(self): """ When not playing, store a pixbuf of the current viewer image. This will allow it to be restored for the transformation box. """ if self.box and self.zoom != 1.0: # The transformation box is active and dezoomed # crop away 1 pixel border to avoid artefacts on the pixbuf self.pixbuf = Gdk.pixbuf_get_from_window(self.get_window(), self.box.area.x + 1, self.box.area.y + 1, self.box.area.width - 2, self.box.area.height - 2) else: self.pixbuf = Gdk.pixbuf_get_from_window( self.get_window(), 0, 0, self.get_window().get_width(), self.get_window().get_height()) self.stored = True def button_release_event(self, unused_widget, event): if event.button == 1: self.box.update_effect_properties() self.box.release_point() self.seeker.flush() self.stored = False return True def button_press_event(self, unused_widget, event): if event.button == 1: self.box.select_point(event) return True def _currentStateCb(self, unused_pipeline, unused_state): self.fixme("TransformationBox disabled") """ self.pipeline = pipeline if state == Gst.State.PAUSED: self._store_pixbuf() self.renderbox() """ def motion_notify_event(self, unused_widget, event): if event.get_state() & Gdk.ModifierType.BUTTON1_MASK: if self.box.transform(event): if self.stored: self.renderbox() return True def do_expose_event(self, event): self.area = event.area if self.box: self._update_gradient() if self.zoom != 1.0: width = int(float(self.area.width) * self.zoom) height = int(float(self.area.height) * self.zoom) area = ((self.area.width - width) / 2, (self.area.height - height) / 2, width, height) self.sink.set_render_rectangle(*area) else: area = self.area self.box.update_size(area) self.renderbox() def _update_gradient(self): self.gradient_background = cairo.LinearGradient( 0, 0, 0, self.area.height) self.gradient_background.add_color_stop_rgb(0.00, .1, .1, .1) self.gradient_background.add_color_stop_rgb(0.50, .2, .2, .2) self.gradient_background.add_color_stop_rgb(1.00, .5, .5, .5) def renderbox(self): if self.box: cr = self.window.cairo_create() cr.push_group() if self.zoom != 1.0: # draw some nice background for zoom out cr.set_source(self.gradient_background) cr.rectangle(0, 0, self.area.width, self.area.height) cr.fill() # translate the drawing of the zoomed out box cr.translate(self.box.area.x, self.box.area.y) # clear the drawingarea with the last known clean video frame # translate when zoomed out if self.pixbuf: if self.box.area.width != self.pixbuf.get_width(): scale = float(self.box.area.width) / float( self.pixbuf.get_width()) cr.save() cr.scale(scale, scale) cr.set_source_pixbuf(self.pixbuf, 0, 0) cr.paint() if self.box.area.width != self.pixbuf.get_width(): cr.restore() if self.pipeline and self.pipeline.getState() == Gst.State.PAUSED: self.box.draw(cr) cr.pop_group_to_source() cr.paint()
class TitleEditor(Loggable): """ Widget for configuring the selected title. @type app: L{Pitivi} """ def __init__(self, app): Loggable.__init__(self) self.app = app self.action_log = app.action_log self.settings = {} self.source = None self.seeker = Seeker() # Drag attributes self._drag_events = [] self._signals_connected = False self._setting_props = False self._setting_initial_props = False self._children_props_handler = None self._createUI() def _createUI(self): builder = Gtk.Builder() builder.add_from_file(os.path.join(get_ui_dir(), "titleeditor.ui")) builder.connect_signals(self) self.widget = builder.get_object("box1") # To be used by tabsmanager self.infobar = builder.get_object("infobar") self.editing_box = builder.get_object("editing_box") self.textarea = builder.get_object("textview") toolbar = builder.get_object("toolbar") toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR) self.textbuffer = Gtk.TextBuffer() self.textarea.set_buffer(self.textbuffer) self.textbuffer.connect("changed", self._textChangedCb) self.font_button = builder.get_object("fontbutton1") self.foreground_color_button = builder.get_object("fore_text_color") self.background_color_button = builder.get_object("back_color") settings = ["valignment", "halignment", "xpos", "ypos"] for setting in settings: self.settings[setting] = builder.get_object(setting) for n, en in list({ _("Custom"): "position", _("Top"): "top", _("Center"): "center", _("Bottom"): "bottom", _("Baseline"): "baseline" }.items()): self.settings["valignment"].append(en, n) for n, en in list({ _("Custom"): "position", _("Left"): "left", _("Center"): "center", _("Right"): "right" }.items()): self.settings["halignment"].append(en, n) self._deactivate() def _setChildProperty(self, name, value): self.action_log.begin("Title %s change" % name) self._setting_props = True self.source.set_child_property(name, value) self._setting_props = False self.action_log.commit() def _backgroundColorButtonCb(self, widget): color = gdk_rgba_to_argb(widget.get_rgba()) self.debug("Setting title background color to %x", color) self._setChildProperty("foreground-color", color) def _frontTextColorButtonCb(self, widget): color = gdk_rgba_to_argb(widget.get_rgba()) self.debug("Setting title foreground color to %x", color) # TODO: Use set_text_color when we work with TitleSources instead of # TitleClips self._setChildProperty("color", color) def _fontButtonCb(self, widget): font_desc = widget.get_font_desc().to_string() self.debug("Setting font desc to %s", font_desc) self._setChildProperty("font-desc", font_desc) def _activate(self): """ Show the title editing UI widgets and hide the infobar """ self.infobar.hide() self.textarea.show() self.editing_box.show() self._connect_signals() def _deactivate(self): """ Reset the title editor interface to its default look """ self.infobar.show() self.textarea.hide() self.editing_box.hide() self._disconnect_signals() def _setWidgetText(self): res, source_text = self.source.get_child_property("text") text = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) if text == source_text: return False if res is False: # FIXME: sometimes we get a TextOverlay/TitleSource # without a valid text property. This should not happen. source_text = "" self.warning( 'Source did not have a text property, setting it to "" to avoid pango choking up on None' ) self.log("Title text set to %s", source_text) self.textbuffer.set_text(source_text) return True def _updateFromSource(self): if not self.source: # Nothing to update from. return self._setWidgetText() self.settings['xpos'].set_value( self.source.get_child_property("xpos")[1]) self.settings['ypos'].set_value( self.source.get_child_property("ypos")[1]) self.settings['valignment'].set_active_id( self.source.get_child_property("valignment")[1].value_name) self.settings['halignment'].set_active_id( self.source.get_child_property("halignment")[1].value_name) font_desc = Pango.FontDescription.from_string( self.source.get_child_property("font-desc")[1]) self.font_button.set_font_desc(font_desc) color = argb_to_gdk_rgba(self.source.get_child_property("color")[1]) self.foreground_color_button.set_rgba(color) color = argb_to_gdk_rgba( self.source.get_child_property("foreground-color")[1]) self.background_color_button.set_rgba(color) def _textChangedCb(self, unused_updated_obj): if not self.source: # Nothing to update. return text = self.textbuffer.get_text(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter(), True) self.log("Source text updated to %s", text) self._setChildProperty("text", text) def _updateSource(self, updated_obj): """ Handle changes in one of the advanced property widgets at the bottom """ if not self.source: # Nothing to update. return for name, obj in list(self.settings.items()): if obj == updated_obj: if name == "valignment": value = getattr(GES.TextVAlign, obj.get_active_id().upper()) visible = obj.get_active_id() == "position" self.settings["ypos"].set_visible(visible) elif name == "halignment": value = getattr(GES.TextHAlign, obj.get_active_id().upper()) visible = obj.get_active_id() == "position" self.settings["xpos"].set_visible(visible) else: value = obj.get_value() self._setChildProperty(name, value) return def set_source(self, source): """ Set the clip to be edited with this editor. @type source: L{GES.TitleSource} """ self.debug("Source set to %s", source) self._deactivate() assert isinstance(source, GES.TextOverlay) or \ isinstance(source, GES.TitleSource) self.source = source self._updateFromSource() self._activate() def unset_source(self): self._deactivate() self.source = None def _createCb(self, unused_button): """ The user clicked the "Create and insert" button, initialize the UI """ clip = GES.TitleClip() clip.set_text("") clip.set_duration(int(Gst.SECOND * 5)) # TODO: insert on the current layer at the playhead position. # If no space is available, create a new layer to insert to on top. self.app.gui.timeline_ui.insertEnd([clip]) self.app.gui.timeline_ui.timeline.selection.setToObj(clip, SELECT) self._setting_initial_props = True clip.set_color(FOREGROUND_DEFAULT_COLOR) clip.set_background(BACKGROUND_DEFAULT_COLOR) self._setting_initial_props = False def _propertyChangedCb(self, source, unused_gstelement, pspec): if self._setting_initial_props: return if self._setting_props: self.seeker.flush() return flush = False if pspec.name == "text": if self._setWidgetText() is True: flush = True elif pspec.name in ["xpos", "ypos"]: value = self.source.get_child_property(pspec.name)[1] if self.settings[pspec.name].get_value() == value: return flush = True self.settings[pspec.name].set_value(value) elif pspec.name in ["valignment", "halignment"]: value = self.source.get_child_property(pspec.name)[1].value_name if self.settings[pspec.name].get_active_id() == value: return flush = True self.settings[pspec.name].set_active_id(value) elif pspec.name == "font-desc": value = self.source.get_child_property("font-desc")[1] if self.font_button.get_font_desc() == value: return flush = True font_desc = Pango.FontDescription.from_string(value) self.font_button.set_font_desc(font_desc) elif pspec.name == "color": color = argb_to_gdk_rgba( self.source.get_child_property("color")[1]) if color == self.foreground_color_button.get_rgba(): return flush = True self.foreground_color_button.set_rgba(color) elif pspec.name == "foreground-color": color = argb_to_gdk_rgba( self.source.get_child_property("foreground-color")[1]) if color == self.background_color_button.get_rgba(): return flush = True self.background_color_button.set_rgba(color) if flush is True: self.seeker.flush() def _connect_signals(self): if self.source and not self._children_props_handler: self._children_props_handler = self.source.connect( 'deep-notify', self._propertyChangedCb) if not self._signals_connected: self.app.gui.viewer.target.connect("motion-notify-event", self.drag_notify_event) self.app.gui.viewer.target.connect("button-press-event", self.drag_press_event) self.app.gui.viewer.target.connect("button-release-event", self.drag_release_event) self._signals_connected = True def _disconnect_signals(self): if self._children_props_handler is not None: self.source.disconnect(self._children_props_handler) self._children_props_handler = None if not self._signals_connected: return self.app.gui.viewer.target.disconnect_by_func(self.drag_notify_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_press_event) self.app.gui.viewer.target.disconnect_by_func(self.drag_release_event) self._signals_connected = False def drag_press_event(self, unused_widget, event): if event.button == 1: self._drag_events = [(event.x, event.y)] # Update drag by drag event change, but not too often self.timeout = GLib.timeout_add(100, self.drag_update_event) # If drag goes out for 0.3 second, and do not come back, consider # drag end self._drag_updated = True self.timeout = GLib.timeout_add(1000, self.drag_possible_end_event) def drag_possible_end_event(self): if self._drag_updated: # Updated during last timeout, wait more self._drag_updated = False return True else: # Not updated - posibly out of bounds, stop drag self.log("Drag timeout") self._drag_events = [] return False def drag_update_event(self): if len(self._drag_events) > 0: st = self._drag_events[0] self._drag_events = [self._drag_events[-1]] e = self._drag_events[0] xdiff = e[0] - st[0] ydiff = e[1] - st[1] xdiff /= self.app.gui.viewer.target.get_allocated_width() ydiff /= self.app.gui.viewer.target.get_allocated_height() newxpos = self.settings["xpos"].get_value() + xdiff newypos = self.settings["ypos"].get_value() + ydiff self.settings["xpos"].set_value(newxpos) self.settings["ypos"].set_value(newypos) return True else: return False def drag_notify_event(self, unused_widget, event): if len(self._drag_events ) > 0 and event.get_state() & Gdk.ModifierType.BUTTON1_MASK: self._drag_updated = True self._drag_events.append((event.x, event.y)) st = self._drag_events[0] e = self._drag_events[-1] def drag_release_event(self, unused_widget, unused_event): self._drag_events = [] def tabSwitchedCb(self, unused_notebook, page_widget, unused_page_index): if self.widget == page_widget: self._connect_signals() else: self._disconnect_signals() def selectionChangedCb(self, selection): selected_clip = selection.getSingleClip(GES.TitleClip) source = None if selected_clip: source = selected_clip.get_children(False)[0] if source: self.set_source(source) else: self.unset_source()
class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable): """ Widget for displaying the ruler. Displays a series of consecutive intervals. For each interval its beginning time is shown. If zoomed in enough, shows the frames in alternate colors. """ __gsignals__ = { "button-press-event": "override", "button-release-event": "override", "motion-notify-event": "override", "scroll-event": "override", "seek": (GObject.SignalFlags.RUN_LAST, None, [GObject.TYPE_UINT64]) } def __init__(self, timeline, hadj): Gtk.DrawingArea.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.log("Creating new ScaleRuler") # Allows stealing focus from other GTK widgets, prevent accidents: self.props.can_focus = True self.connect("focus-in-event", self._focusInCb) self.connect("focus-out-event", self._focusOutCb) self.timeline = timeline self._background_color = timeline.get_style_context().lookup_color( 'theme_bg_color')[1] self._seeker = Seeker() self.hadj = hadj hadj.connect("value-changed", self._hadjValueChangedCb) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.SCROLL_MASK) self.pixbuf = None # all values are in pixels self.pixbuf_offset = 0 self.pixbuf_offset_painted = 0 # This is the number of width we allocate for the pixbuf self.pixbuf_multiples = 4 self.position = 0 # In nanoseconds self.pressed = False self.frame_rate = Gst.Fraction(1 / 1) self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.connect('draw', self.drawCb) self.connect('configure-event', self.configureEventCb) self.callback_id = None self.callback_id_scroll = None self.set_size_request(0, HEIGHT) style = self.get_style_context() color_normal = style.get_color(Gtk.StateFlags.NORMAL) color_insensitive = style.get_color(Gtk.StateFlags.INSENSITIVE) self._color_normal = color_normal self._color_dimmed = Gdk.RGBA( *[(x * 3 + y * 2) / 5 for x, y in ((color_normal.red, color_insensitive.red), (color_normal.green, color_insensitive.green), (color_normal.blue, color_insensitive.blue))]) self.scales = SCALES def _focusInCb(self, unused_widget, unused_arg): self.log("Ruler has grabbed focus") self.timeline.setActionsSensitivity(True) def _focusOutCb(self, unused_widget, unused_arg): self.log("Ruler has lost focus") self.timeline.setActionsSensitivity(False) def _hadjValueChangedCb(self, unused_arg): self.pixbuf_offset = self.hadj.get_value() if self.callback_id_scroll is not None: GLib.source_remove(self.callback_id_scroll) self.callback_id_scroll = GLib.timeout_add(100, self._maybeUpdate) # Zoomable interface override def _maybeUpdate(self): self.queue_draw() self.callback_id = None self.callback_id_scroll = None return False def zoomChanged(self): if self.callback_id is not None: GLib.source_remove(self.callback_id) self.callback_id = GLib.timeout_add(100, self._maybeUpdate) # Timeline position changed method def setPipeline(self, pipeline): pipeline.connect('position', self.timelinePositionCb) def timelinePositionCb(self, unused_pipeline, position): self.position = position self.queue_draw() # Gtk.Widget overrides def configureEventCb(self, widget, unused_event, unused_data=None): width = widget.get_allocated_width() height = widget.get_allocated_height() self.debug("Configuring, height %d, width %d", width, height) # Destroy previous buffer if self.pixbuf is not None: self.pixbuf.finish() self.pixbuf = None # Create a new buffer self.height = height self.pixbuf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) return False def drawCb(self, unused_widget, context): if self.pixbuf is None: self.info('No buffer to paint') return False self.drawBackground(context) self.drawRuler(context) self.drawPosition(context) return False def do_button_press_event(self, event): self.debug("button pressed at x:%d", event.x) self.pressed = True position = self.pixelToNs(event.x + self.pixbuf_offset) self._seeker.seek(position, on_idle=True) return True def do_button_release_event(self, event): self.debug("button released at x:%d", event.x) self.grab_focus() # Prevent other widgets from being confused self.pressed = False return False def do_motion_notify_event(self, event): position = self.pixelToNs(event.x + self.pixbuf_offset) if self.pressed: self.debug("motion at event.x %d", event.x) self._seeker.seek(position, on_idle=True) human_time = beautify_length(position) cur_frame = int(position / self.ns_per_frame) + 1 self.set_tooltip_text(human_time + "\n" + _("Frame #%d" % cur_frame)) return False def do_scroll_event(self, event): if event.scroll.state & Gdk.ModifierType.CONTROL_MASK: # Control + scroll = zoom if event.scroll.direction == Gdk.ScrollDirection.UP: Zoomable.zoomIn() self.timeline.zoomed_fitted = False elif event.scroll.direction == Gdk.ScrollDirection.DOWN: Zoomable.zoomOut() self.timeline.zoomed_fitted = False else: # No modifier key held down, just scroll if (event.scroll.direction == Gdk.ScrollDirection.UP or event.scroll.direction == Gdk.ScrollDirection.LEFT): self.timeline.scroll_left() elif (event.scroll.direction == Gdk.ScrollDirection.DOWN or event.scroll.direction == Gdk.ScrollDirection.RIGHT): self.timeline.scroll_right() def setProjectFrameRate(self, rate): """ Set the lowest scale based on project framerate """ self.frame_rate = rate self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND self.scales = (float(2 / rate), float(5 / rate), float( 10 / rate)) + SCALES # Drawing methods def drawBackground(self, context): style = self.get_style_context() set_cairo_color(context, self._background_color) width = context.get_target().get_width() height = context.get_target().get_height() context.rectangle(0, 0, width, height) context.fill() offset = int(self.nsToPixel(Gst.CLOCK_TIME_NONE)) - self.pixbuf_offset if offset > 0: set_cairo_color(context, style.get_background_color(Gtk.StateFlags.ACTIVE)) context.rectangle(0, 0, int(offset), height) context.fill() def drawRuler(self, context): context.set_font_face(NORMAL_FONT) context.set_font_size(NORMAL_FONT_SIZE) spacing, scale = self._getSpacing(context) offset = self.pixbuf_offset % spacing self.drawFrameBoundaries(context) self.drawTicks(context, offset, spacing) self.drawTimes(context, offset, spacing, scale) def _getSpacing(self, context): textwidth = context.text_extents(time_to_string(0))[2] zoom = Zoomable.zoomratio for scale in self.scales: spacing = scale * zoom if spacing >= textwidth * 1.5: return spacing, scale raise Exception( "Failed to find an interval size for textwidth:%s, zoomratio:%s" % (textwidth, Zoomable.zoomratio)) def drawTicks(self, context, offset, spacing): for count_per_interval, height_ratio in TICK_TYPES: space = float(spacing) / count_per_interval if space < MIN_TICK_SPACING_PIXELS: break paintpos = 0.5 - offset set_cairo_color(context, self._color_normal) while paintpos < context.get_target().get_width(): self._drawTick(context, paintpos, height_ratio) paintpos += space def _drawTick(self, context, paintpos, height_ratio): # We need to use 0.5 pixel offsets to get a sharp 1 px line in cairo paintpos = int(paintpos - 0.5) + 0.5 y = self.height * (1.0 - height_ratio) context.set_line_width(1) context.move_to(paintpos, y) context.line_to(paintpos, self.height) context.close_path() context.stroke() def drawTimes(self, context, offset, spacing, scale): # figure out what the optimal offset is interval = int(Gst.SECOND * scale) current_time = self.pixelToNs(self.pixbuf_offset) paintpos = TIMES_LEFT_MARGIN_PIXELS if offset > 0: current_time = current_time - (current_time % interval) + interval paintpos += spacing - offset state = Gtk.StateFlags.NORMAL style = self.get_style_context() set_cairo_color(context, style.get_color(state)) y_bearing = context.text_extents("0")[1] millis = scale < 1 def split(x): # Seven elements: h : mm : ss . mmm # Using negative indices because the first element (hour) # can have a variable length. return x[:-10], x[-10], x[-9:-7], x[-7], x[-6:-4], x[-4], x[-3:] previous = split(time_to_string(max(0, current_time - interval))) width = context.get_target().get_width() while paintpos < width: context.move_to(int(paintpos), 1 - y_bearing) current = split(time_to_string(int(current_time))) self._drawTime(context, current, previous, millis) previous = current paintpos += spacing current_time += interval def _drawTime(self, context, current, previous, millis): hour = int(current[0]) for index, (element, previous_element) in enumerate(zip(current, previous)): if index <= 1 and not hour: continue if index >= 5 and not millis: break if element == previous_element: color = self._color_dimmed else: color = self._color_normal set_cairo_color(context, color) # Display the millis with a smaller font small = index >= 5 if small: context.set_font_size(SMALL_FONT_SIZE) context.show_text(element) if small: context.set_font_size(NORMAL_FONT_SIZE) def drawFrameBoundaries(self, context): """ Draw the alternating rectangles that represent the project frames at high zoom levels. These are based on the framerate set in the project settings, not the actual frames on a video codec level. """ frame_width = self.nsToPixel(self.ns_per_frame) if not frame_width >= FRAME_MIN_WIDTH_PIXELS: return offset = self.pixbuf_offset % frame_width height = context.get_target().get_height() y = int(height - FRAME_HEIGHT_PIXELS) # INSENSITIVE is a dark shade of gray, but lacks contrast # SELECTED will be bright blue and more visible to represent frames style = self.get_style_context() states = [ style.get_background_color(Gtk.StateFlags.ACTIVE), style.get_background_color(Gtk.StateFlags.SELECTED) ] frame_num = int( self.pixelToNs(self.pixbuf_offset) * float(self.frame_rate) / Gst.SECOND) paintpos = self.pixbuf_offset - offset max_pos = context.get_target().get_width() + self.pixbuf_offset while paintpos < max_pos: paintpos = self.nsToPixel(1 / float(self.frame_rate) * Gst.SECOND * frame_num) set_cairo_color(context, states[(frame_num + 1) % 2]) context.rectangle(0.5 + paintpos - self.pixbuf_offset, y, frame_width, height) context.fill() frame_num += 1 def drawPosition(self, context): # Add 0.5 so that the line center is at the middle of the pixel, # without this the line appears blurry. xpos = self.nsToPixel(self.position) - self.pixbuf_offset + 0.5 context.set_line_width(PLAYHEAD_WIDTH + 2) set_cairo_color(context, PLAYHEAD_COLOR) context.move_to(xpos, 0) context.line_to(xpos, context.get_target().get_height()) context.stroke()
class ViewerWidget(Gtk.AspectFrame, Loggable): """ Widget for displaying a GStreamer video sink. @ivar settings: The settings of the application. @type settings: L{GlobalSettings} """ __gsignals__ = {} def __init__(self, settings=None, realizedCb=None): # Prevent black frames and flickering while resizing or changing focus: # The aspect ratio gets overridden by setDisplayAspectRatio. Gtk.AspectFrame.__init__(self, xalign=0.5, yalign=1.0, ratio=4.0 / 3.0, obey_child=False) Loggable.__init__(self) self.drawing_area = GtkClutter.Embed() self.drawing_area.set_double_buffered(False) # We keep the ViewerWidget hidden initially, or the desktop wallpaper # would show through the non-double-buffered widget! if realizedCb: self.drawing_area.connect("realize", realizedCb, self) self.add(self.drawing_area) layout_manager = Clutter.BinLayout(x_align=Clutter.BinAlignment.FILL, y_align=Clutter.BinAlignment.FILL) self.drawing_area.get_stage().set_layout_manager(layout_manager) self.texture = Clutter.Texture() # This is a trick to make the viewer appear darker at the start. self.texture.set_from_rgb_data(data=[0] * 3, has_alpha=False, width=1, height=1, rowstride=3, bpp=3, flags=Clutter.TextureFlags.NONE) self.drawing_area.get_stage().add_child(self.texture) self.drawing_area.show() self.seeker = Seeker() self.settings = settings self.box = None self.stored = False self.area = None self.zoom = 1.0 self.sink = None self.pixbuf = None self.pipeline = None self.transformation_properties = None # FIXME PyGi Styling with Gtk3 #for state in range(Gtk.StateType.INSENSITIVE + 1): #self.modify_bg(state, self.style.black) def setDisplayAspectRatio(self, ratio): self.set_property("ratio", float(ratio)) def init_transformation_events(self): self.fixme("TransformationBox disabled") """ self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK) """ def show_box(self): self.fixme("TransformationBox disabled") """ if not self.box: self.box = TransformationBox(self.settings) self.box.init_size(self.area) self._update_gradient() self.connect("button-press-event", self.button_press_event) self.connect("button-release-event", self.button_release_event) self.connect("motion-notify-event", self.motion_notify_event) self.connect("size-allocate", self._sizeCb) self.box.set_transformation_properties(self.transformation_properties) self.renderbox() """ def _sizeCb(self, unused_widget, unused_area): # The transformation box is cleared when using regular rendering # so we need to flush the pipeline self.seeker.flush() def hide_box(self): if self.box: self.box = None self.disconnect_by_func(self.button_press_event) self.disconnect_by_func(self.button_release_event) self.disconnect_by_func(self.motion_notify_event) self.seeker.flush() self.zoom = 1.0 if self.sink: self.sink.set_render_rectangle(*self.area) def set_transformation_properties(self, transformation_properties): self.transformation_properties = transformation_properties def _store_pixbuf(self): """ When not playing, store a pixbuf of the current viewer image. This will allow it to be restored for the transformation box. """ if self.box and self.zoom != 1.0: # The transformation box is active and dezoomed # crop away 1 pixel border to avoid artefacts on the pixbuf self.pixbuf = Gdk.pixbuf_get_from_window(self.get_window(), self.box.area.x + 1, self.box.area.y + 1, self.box.area.width - 2, self.box.area.height - 2) else: self.pixbuf = Gdk.pixbuf_get_from_window(self.get_window(), 0, 0, self.get_window().get_width(), self.get_window().get_height()) self.stored = True def button_release_event(self, unused_widget, event): if event.button == 1: self.box.update_effect_properties() self.box.release_point() self.seeker.flush() self.stored = False return True def button_press_event(self, unused_widget, event): if event.button == 1: self.box.select_point(event) return True def _currentStateCb(self, unused_pipeline, unused_state): self.fixme("TransformationBox disabled") """ self.pipeline = pipeline if state == Gst.State.PAUSED: self._store_pixbuf() self.renderbox() """ def motion_notify_event(self, unused_widget, event): if event.get_state() & Gdk.ModifierType.BUTTON1_MASK: if self.box.transform(event): if self.stored: self.renderbox() return True def do_expose_event(self, event): self.area = event.area if self.box: self._update_gradient() if self.zoom != 1.0: width = int(float(self.area.width) * self.zoom) height = int(float(self.area.height) * self.zoom) area = ((self.area.width - width) / 2, (self.area.height - height) / 2, width, height) self.sink.set_render_rectangle(*area) else: area = self.area self.box.update_size(area) self.renderbox() def _update_gradient(self): self.gradient_background = cairo.LinearGradient(0, 0, 0, self.area.height) self.gradient_background.add_color_stop_rgb(0.00, .1, .1, .1) self.gradient_background.add_color_stop_rgb(0.50, .2, .2, .2) self.gradient_background.add_color_stop_rgb(1.00, .5, .5, .5) def renderbox(self): if self.box: cr = self.window.cairo_create() cr.push_group() if self.zoom != 1.0: # draw some nice background for zoom out cr.set_source(self.gradient_background) cr.rectangle(0, 0, self.area.width, self.area.height) cr.fill() # translate the drawing of the zoomed out box cr.translate(self.box.area.x, self.box.area.y) # clear the drawingarea with the last known clean video frame # translate when zoomed out if self.pixbuf: if self.box.area.width != self.pixbuf.get_width(): scale = float(self.box.area.width) / float(self.pixbuf.get_width()) cr.save() cr.scale(scale, scale) cr.set_source_pixbuf(self.pixbuf, 0, 0) cr.paint() if self.box.area.width != self.pixbuf.get_width(): cr.restore() if self.pipeline and self.pipeline.getState() == Gst.State.PAUSED: self.box.draw(cr) cr.pop_group_to_source() cr.paint()