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 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 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, 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 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()
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()