class TimelineControls(gtk.VBox): def __init__(self): gtk.VBox.__init__(self) self._tracks = [] self.set_spacing(LAYER_SPACING) self.set_size_request(TRACK_CONTROL_WIDTH, -1) ## Timeline callbacks def _set_timeline(self): while self._tracks: self._trackRemoved(None, 0) if self.timeline: for track in self.timeline.tracks: self._trackAdded(None, track) timeline = receiver(_set_timeline) @handler(timeline, "track-added") def _trackAdded(self, timeline, track): track = TrackControls(track) self._tracks.append(track) self.pack_start(track, False, False) track.show() @handler(timeline, "track-removed") def _trackRemoved(self, unused_timeline, position): track = self._tracks[position] del self._tracks[position] self.remove(track)
class TrimHandle(View, goocanvas.Rect, Zoomable): """A component of a TimelineObject which manage's the source's edit points""" element = receiver() def __init__(self, element, timeline, **kwargs): self.element = element self.timeline = timeline goocanvas.Rect.__init__(self, width=5, fill_color_rgba=0x00000022, line_width=0, **kwargs) View.__init__(self) Zoomable.__init__(self)
class Transition(goocanvas.Rect, Zoomable): def __init__(self, transition): goocanvas.Rect.__init__(self) Zoomable.__init__(self) self.props.fill_color_rgba = 0xFFFFFF99 self.props.stroke_color_rgba = 0x00000099 self.set_simple_transform(0, -LAYER_SPACING + 3, 1.0, 0) self.props.height = LAYER_SPACING - 6 self.props.pointer_events = goocanvas.EVENTS_NONE self.props.radius_x = 2 self.props.radius_y = 2 self.transition = transition def _setTransition(self): if self.transition: self._updateAll() def _updateAll(self): transition = self.transition start = transition.start duration = transition.duration priority = transition.priority self._updateStart(transition, start) self._updateDuration(transition, duration) self._updatePriority(transition, priority) transition = receiver(_setTransition) @handler(transition, "start-changed") def _updateStart(self, transition, start): self.props.x = self.nsToPixel(start) @handler(transition, "duration-changed") def _updateDuration(self, transition, duration): width = max(0, self.nsToPixel(duration)) if width == 0: self.props.visibility = goocanvas.ITEM_INVISIBLE else: self.props.visibility = goocanvas.ITEM_VISIBLE self.props.width = width @handler(transition, "priority-changed") def _updatePriority(self, transition, priority): self.props.y = (LAYER_HEIGHT_EXPANDED + LAYER_SPACING) * priority def zoomChanged(self): self._updateAll()
class TrackControls(gtk.Label): __gtype_name__ = 'TrackControls' def __init__(self, track): gtk.Label.__init__(self) self.set_alignment(0.5, 0.1) self.set_markup(track_name(track)) self.track = track self.set_size_request(TRACK_CONTROL_WIDTH, LAYER_HEIGHT_EXPANDED) def _setTrack(self): if self.track: self._maxPriorityChanged(None, self.track.max_priority) track = receiver(_setTrack) @handler(track, "max-priority-changed") def _maxPriorityChanged(self, track, max_priority): self.set_size_request(TRACK_CONTROL_WIDTH, (1 + max_priority) * (LAYER_HEIGHT_EXPANDED + LAYER_SPACING))
class TrimHandle(View, goocanvas.Image, Zoomable): """A component of a TrackObject which manage's the source's edit points""" element = receiver() def __init__(self, instance, element, timeline, **kwargs): self.app = instance self.element = element self.timeline = timeline goocanvas.Image.__init__(self, pixbuf=TRIMBAR_PIXBUF, line_width=0, **kwargs) View.__init__(self) Zoomable.__init__(self) def focus(self): self.props.pixbuf = TRIMBAR_PIXBUF_FOCUS def unfocus(self): self.props.pixbuf = TRIMBAR_PIXBUF
class Track(goocanvas.Group, Zoomable): __gtype_name__ = 'Track' def __init__(self, instance, track, timeline=None): goocanvas.Group.__init__(self) Zoomable.__init__(self) self.app = instance self.widgets = {} self.timeline = timeline self.track = track self.max_priority = 0 self._expanded = True ## Properties def setExpanded(self, expanded): if expanded != self._expanded: self._expanded = expanded for widget in self.widgets.itervalues(): widget.expanded = expanded self.get_canvas().regroupTracks() def getHeight(self): if self._expanded: return (1 + self.track.max_priority) * (LAYER_HEIGHT_EXPANDED + LAYER_SPACING) else: return LAYER_HEIGHT_COLLAPSED + LAYER_SPACING height = property(getHeight) ## Public API ## track signals def _setTrack(self): if self.track: for trackobj in self.track.track_objects: if trackobj is self.track.default_track_object: continue self._objectAdded(None, trackobj) track = receiver(_setTrack) @handler(track, "track-object-added") def _objectAdded(self, unused_timeline, track_object): w = TrackObject(self.app, track_object, self.track, self.timeline) self.widgets[track_object] = w self.add_child(w) @handler(track, "track-object-removed") def _objectRemoved(self, unused_timeline, track_object): w = self.widgets[track_object] self.remove_child(w) del self.widgets[track_object] Zoomable.removeInstance(w) @handler(track, "max-priority-changed") def _maxPriorityChanged(self, track, max_priority): self.get_canvas().regroupTracks()
class TrackObject(View, goocanvas.Group, Zoomable): class Controller(TimelineController): def drag_start(self, item, target, event): TimelineController.drag_start(self, item, target, event) self._view.raise_(None) self._context = MoveContext( self._view.timeline, self._view.element, self._view.timeline.selection.getSelectedTrackObjs()) self._view.app.action_log.begin("move object") def _getMode(self): if self._shift_down: return self._context.RIPPLE return self._context.DEFAULT def click(self, pos): mode = SELECT if self._last_event.get_state() & gtk.gdk.SHIFT_MASK: mode = SELECT_ADD elif self._last_event.get_state() & gtk.gdk.CONTROL_MASK: mode = UNSELECT self._view.timeline.setSelectionToObj(self._view.element, mode) def __init__(self, instance, element, track, timeline): goocanvas.Group.__init__(self) View.__init__(self) Zoomable.__init__(self) self.app = instance self.track = track self.timeline = timeline self.namewidth = 0 self.nameheight = 0 self.bg = goocanvas.Rect(height=self.height, line_width=0) self.content = Preview(element) self.name = goocanvas.Text(x=NAME_HOFFSET + NAME_PADDING, y=NAME_VOFFSET + NAME_PADDING, operator=cairo.OPERATOR_ADD, alignment=pango.ALIGN_LEFT) self.namebg = goocanvas.Rect(radius_x=2, radius_y=2, x=NAME_HOFFSET, y=NAME_VOFFSET, line_width=0) self.start_handle = StartHandle(self.app, element, timeline, height=self.height) self.end_handle = EndHandle(self.app, element, timeline, height=self.height) self.selection_indicator = goocanvas.Rect( visibility=goocanvas.ITEM_INVISIBLE, line_width=0.0, height=self.height) for thing in (self.bg, self.content, self.selection_indicator, self.start_handle, self.end_handle, self.namebg, self.name): self.add_child(thing) for prop, interpolator in element.getInterpolators().itervalues(): self.add_child(Curve(instance, element, interpolator, 50)) self.element = element self.settings = instance.settings self.normal() ## Properties _height = LAYER_HEIGHT_EXPANDED def setHeight(self, height): self._height = height self.start_handle.props.height = height self.end_handle.props.height = height self._update() def getHeight(self): return self._height height = property(getHeight, setHeight) _expanded = True def setExpanded(self, expanded): self._expanded = expanded if not self._expanded: self.height = LAYER_HEIGHT_COLLAPSED self.content.props.visibility = goocanvas.ITEM_INVISIBLE self.namebg.props.visibility = goocanvas.ITEM_INVISIBLE self.bg.props.height = LAYER_HEIGHT_COLLAPSED self.name.props.y = 0 else: self.height = LAYER_HEIGHT_EXPANDED self.content.props.visibility = goocanvas.ITEM_VISIBLE self.namebg.props.visibility = goocanvas.ITEM_VISIBLE self.bg.props.height = LAYER_HEIGHT_EXPANDED self.height = LAYER_HEIGHT_EXPANDED self.name.props.y = NAME_VOFFSET + NAME_PADDING def getExpanded(self): return self._expanded expanded = property(getExpanded, setExpanded) ## Public API def focus(self): self.start_handle.focus() self.end_handle.focus() def unfocus(self): self.start_handle.unfocus() self.end_handle.unfocus() def zoomChanged(self): self._update() ## settings signals def _setSettings(self): if self.settings: self.clipAppearanceSettingsChanged() settings = receiver(_setSettings) @handler(settings, "audioClipBgChanged") @handler(settings, "videoClipBgChanged") @handler(settings, "selectedColorChanged") @handler(settings, "clipFontDescChanged") def clipAppearanceSettingsChanged(self, *args): if isinstance(self.element.stream, VideoStream): color = self.settings.videoClipBg elif isinstance(self.element.stream, AudioStream): color = self.settings.audioClipBg pattern = unpack_cairo_gradient(color) self.bg.props.fill_pattern = pattern self.namebg.props.fill_pattern = pattern self.selection_indicator.props.fill_pattern = unpack_cairo_pattern( self.settings.selectedColor) self.name.props.font = self.settings.clipFontDesc self.name.props.fill_pattern = unpack_cairo_pattern( self.settings.clipFontColor) twidth, theight = text_size(self.name) self.namewidth = twidth self.nameheight = theight self._update() ## element signals def _setElement(self): if self.element: self.name.props.text = os.path.basename( unquote(self.element.factory.name)) twidth, theight = text_size(self.name) self.namewidth = twidth self.nameheight = theight self._update() element = receiver(_setElement) @handler(element, "start-changed") @handler(element, "duration-changed") def startChangedCb(self, track_object, start): self._update() @handler(element, "selected-changed") def selected_changed(self, element, state): if element.selected: self.selection_indicator.props.visibility = goocanvas.ITEM_VISIBLE else: self.selection_indicator.props.visibility = \ goocanvas.ITEM_INVISIBLE @handler(element, "priority-changed") def priority_changed(self, element, priority): self._update() def _update(self): try: x = self.nsToPixel(self.element.start) except Exception, e: print self.element.start raise Exception(e) y = (self.height + LAYER_SPACING) * self.element.priority self.set_simple_transform(x, y, 1, 0) width = self.nsToPixel(self.element.duration) w = width - self.end_handle.props.width self.name.props.clip_path = "M%g,%g h%g v%g h-%g z" % (0, 0, w, self.height, w) self.bg.props.width = width self.selection_indicator.props.width = width self.end_handle.props.x = w if self.expanded: if w - NAME_HOFFSET > 0: self.namebg.props.height = self.nameheight + NAME_PADDING2X self.namebg.props.width = min(w - NAME_HOFFSET, self.namewidth + NAME_PADDING2X) self.namebg.props.visibility = goocanvas.ITEM_VISIBLE else: self.namebg.props.visibility = goocanvas.ITEM_INVISIBLE
class PitiviMainWindow(gtk.Window, Loggable): """ Pitivi's main window. @cvar app: The application object @type app: L{Application} @cvar project: The current project @type project: L{Project} """ def __init__(self, instance): """ initialize with the Pitivi object """ gtk.Window.__init__(self) Loggable.__init__(self) self.log("Creating MainWindow") self.actions = None self.toggleactions = None self.actiongroup = None self.settings = instance.settings self.is_fullscreen = self.settings.mainWindowFullScreen self.timelinepos = 0 self.prefsdialog = None create_stock_icons() self._setActions(instance) self._createUi(instance) self.app = instance self._zoom_duration_changed = False self.app.projectManager.connect("new-project-loading", self._projectManagerNewProjectLoadingCb) self.app.projectManager.connect("new-project-loaded", self._projectManagerNewProjectLoadedCb) self.app.projectManager.connect("new-project-failed", self._projectManagerNewProjectFailedCb) self.app.projectManager.connect("save-project-failed", self._projectManagerSaveProjectFailedCb) self.app.projectManager.connect("project-saved", self._projectManagerProjectSavedCb) self.app.projectManager.connect("closing-project", self._projectManagerClosingProjectCb) self.app.projectManager.connect("reverting-to-saved", self._projectManagerRevertingToSavedCb) self.app.projectManager.connect("project-closed", self._projectManagerProjectClosedCb) self.app.projectManager.connect("missing-uri", self._projectManagerMissingUriCb) self.app.action_log.connect("commit", self._actionLogCommit) self.app.action_log.connect("undo", self._actionLogUndo) self.app.action_log.connect("redo", self._actionLogRedo) self.app.action_log.connect("cleaned", self._actionLogCleaned) # if no webcams available, hide the webcam action if self.app.deviceprobe is not None: # On Windows disable device probe if platform.system() != 'Windows': self.app.deviceprobe.connect("device-added", self._deviceChangeCb) self.app.deviceprobe.connect("device-removed", self._deviceChangeCb) if len(self.app.deviceprobe.getVideoSourceDevices()) < 1: self.webcam_button.set_sensitive(False) else: self.webcam_button.set_sensitive(False) self.show() def showEncodingDialog(self, project, pause=True): """ Shows the L{EncodingDialog} for the given project Timeline. @param project: The project @type project: L{Project} @param pause: If C{True}, pause the timeline before displaying the dialog. @type pause: C{bool} """ from encodingdialog import EncodingDialog if pause: project.pipeline.pause() win = EncodingDialog(self, project) win.window.connect("destroy", self._encodingDialogDestroyCb) self.set_sensitive(False) win.show() def _encodingDialogDestroyCb(self, unused_dialog): self.set_sensitive(True) def _recordCb(self, unused_button): self.showEncodingDialog(self.project) def _setActions(self, instance): PLAY = _("Start Playback") PAUSE = _("Stop Playback") LOOP = _("Loop over selected area") """ sets up the GtkActions """ self.actions = [ ("NewProject", gtk.STOCK_NEW, None, None, _("Create a new project"), self._newProjectMenuCb), ("OpenProject", gtk.STOCK_OPEN, None, None, _("Open an existing project"), self._openProjectCb), ("SaveProject", gtk.STOCK_SAVE, None, None, _("Save the current project"), self._saveProjectCb), ("SaveProjectAs", gtk.STOCK_SAVE_AS, None, None, _("Save the current project"), self._saveProjectAsCb), ("RevertToSavedProject", gtk.STOCK_REVERT_TO_SAVED, None, None, _("Reload the current project"), self._revertToSavedProjectCb), ("ProjectSettings", gtk.STOCK_PROPERTIES, _("Project Settings"), None, _("Edit the project settings"), self._projectSettingsCb), ("RenderProject", 'pitivi-render' , _("_Render project"), None, _("Render project"), self._recordCb), ("Undo", gtk.STOCK_UNDO, _("_Undo"), "<Ctrl>Z", _("Undo the last operation"), self._undoCb), ("Redo", gtk.STOCK_REDO, _("_Redo"), "<Ctrl>Y", _("Redo the last operation that was undone"), self._redoCb), ("PluginManager", gtk.STOCK_PREFERENCES , _("_Plugins..."), None, _("Manage plugins"), self._pluginManagerCb), ("Preferences", gtk.STOCK_PREFERENCES, _("_Preferences"), None, None, self._prefsCb), ("ImportfromCam", gtk.STOCK_ADD , _("Import from _Webcam..."), None, _("Import Camera stream"), self._ImportWebcam), ("Screencast", gtk.STOCK_ADD , _("_Make screencast..."), None, _("Capture the desktop"), self._Screencast), ("NetstreamCapture", gtk.STOCK_ADD , _("_Capture Network Stream..."), None, _("Capture Network Stream"), self._ImportNetstream), ("Quit", gtk.STOCK_QUIT, None, None, None, self._quitCb), ("About", gtk.STOCK_ABOUT, None, None, _("Information about %s") % APPNAME, self._aboutCb), ("File", None, _("_File")), ("Edit", None, _("_Edit")), ("View", None, _("_View")), ("Library", None, _("_Project")), ("Timeline", None, _("_Timeline")), ("Viewer", None, _("Previe_w")), ("PlayPause", gtk.STOCK_MEDIA_PLAY, None, "space", PLAY, self.playPause), ("Loop", gtk.STOCK_REFRESH, _("Loop"), None, LOOP, self.loop), ("Help", None, _("_Help")), ] self.toggleactions = [ ("FullScreen", gtk.STOCK_FULLSCREEN, None, "f", _("View the main window on the whole screen"), self._fullScreenCb), ("FullScreenAlternate", gtk.STOCK_FULLSCREEN, None, "F11", None, self._fullScreenAlternateCb), ("ShowHideMainToolbar", None, _("Main Toolbar"), None, None, self._showHideMainToolBar, self.settings.mainWindowShowMainToolbar), ("ShowHideTimelineToolbar", None, _("Timeline Toolbar"), None, None, self._showHideTimelineToolbar, self.settings.mainWindowShowTimelineToolbar), ] self.actiongroup = gtk.ActionGroup("mainwindow") self.actiongroup.add_actions(self.actions) self.actiongroup.add_toggle_actions(self.toggleactions) # deactivating non-functional actions # FIXME : reactivate them save_action = self.actiongroup.get_action("SaveProject") save_action.set_sensitive(False) for action in self.actiongroup.list_actions(): action_name = action.get_name() if action_name == "RenderProject": self.render_button = action # this will be set sensitive when the timeline duration changes action.set_sensitive(False) action.props.is_important = True elif action_name == "ImportfromCam": self.webcam_button = action action.set_sensitive(False) elif action_name == "Screencast": # FIXME : re-enable this action once istanbul integration is complete # and upstream istanbul has applied packages for proper interaction. action.set_sensitive(False) action.set_visible(False) elif action_name in [ "ProjectSettings", "Quit", "File", "Edit", "Help", "About", "View", "FullScreen", "FullScreenAlternate", "ImportSourcesFolder", "PluginManager", "PlayPause", "Project", "FrameForward", "FrameBackward", "ShowHideMainToolbar", "ShowHideTimelineToolbar", "Library", "Timeline", "Viewer", "FrameForward", "FrameBackward", "SecondForward", "SecondBackward", "EdgeForward", "EdgeBackward", "Preferences"]: action.set_sensitive(True) elif action_name in ["NewProject", "SaveProjectAs", "OpenProject"]: if instance.settings.fileSupportEnabled: action.set_sensitive(True) elif action_name == "SaveProject": if instance.settings.fileSupportEnabled: action.set_sensitive(True) action.props.is_important = True elif action_name == "Undo": action.set_sensitive(True) action.props.is_important = True else: action.set_sensitive(False) self.uimanager = gtk.UIManager() self.add_accel_group(self.uimanager.get_accel_group()) self.uimanager.insert_action_group(self.actiongroup, 0) if 'pitivi.exe' in __file__.lower(): xml = LIBDIR + '\\pitivi.exe' else: xml = __file__ self.uimanager.add_ui_from_file(os.path.join(os.path.dirname( os.path.abspath(xml)), "mainwindow.xml")) def _createUi(self, instance): """ Create the graphical interface """ self.set_title("%s" % (APPNAME)) self.connect("delete-event", self._deleteCb) self.connect("configure-event", self._configureCb) # main menu & toolbar vbox = gtk.VBox(False) self.add(vbox) vbox.show() self.menu = self.uimanager.get_widget("/MainMenuBar") vbox.pack_start(self.menu, expand=False) self.menu.show() self.toolbar = self.uimanager.get_widget("/MainToolBar") vbox.pack_start(self.toolbar, expand=False) self.toolbar.show() # timeline and project tabs vpaned = gtk.VPaned() vbox.pack_start(vpaned) vpaned.show() self.timeline = Timeline(instance, self.uimanager) self.timeline.project = self.project vpaned.pack2(self.timeline, resize=True, shrink=False) self.timeline.show() self.mainhpaned = gtk.HPaned() vpaned.pack1(self.mainhpaned, resize=True, shrink=False) self.secondhpaned = gtk.HPaned() self.mainhpaned.pack1(self.secondhpaned, resize=True, shrink=False) self.secondhpaned.show() self.mainhpaned.show() self.projecttabs = BaseTabs(instance) self.sourcelist = SourceList(instance, self.uimanager) self.projecttabs.append_page(self.sourcelist, gtk.Label(_("Media Library"))) self._connectToSourceList() self.sourcelist.show() self.effectlist = EffectList(instance, self.uimanager) self.projecttabs.append_page(self.effectlist, gtk.Label(_("Effect Library"))) self.effectlist.show() self.secondhpaned.pack1(self.projecttabs, resize=True, shrink=False) self.projecttabs.show() #Clips properties self.propertiestabs = BaseTabs(instance, True) self.clipconfig = ClipProperties(instance, self.uimanager) self.clipconfig.project = self.project self.propertiestabs.append_page(self.clipconfig, gtk.Label(_("Effects configurations"))) self.clipconfig.show() self.secondhpaned.pack2(self.propertiestabs, resize= True, shrink=False) self.propertiestabs.show() # Viewer self.viewer = PitiviViewer() # drag and drop self.viewer.drag_dest_set(gtk.DEST_DEFAULT_DROP | gtk.DEST_DEFAULT_MOTION, [dnd.FILESOURCE_TUPLE, dnd.URI_TUPLE], gtk.gdk.ACTION_COPY) self.viewer.connect("drag_data_received", self._viewerDndDataReceivedCb) self.mainhpaned.pack2(self.viewer, resize=False, shrink=False) self.viewer.show() self.viewer.connect("expose-event", self._exposeEventCb) # window and pane position defaults self.mainhpaned = self.mainhpaned self.hpaned = self.secondhpaned self.vpaned = vpaned height = -1 width = -1 if self.settings.mainWindowHPanePosition: self.hpaned.set_position(self.settings.mainWindowHPanePosition) if self.settings.mainWindowMainHPanePosition: self.mainhpaned.set_position(self.settings.mainWindowMainHPanePosition) if self.settings.mainWindowVPanePosition: self.vpaned.set_position(self.settings.mainWindowVPanePosition) if self.settings.mainWindowWidth: width = self.settings.mainWindowWidth if self.settings.mainWindowHeight: height = self.settings.mainWindowHeight self.set_default_size(width, height) if height == -1 and width == -1: self.maximize() self._do_pending_fullscreen = False # FIXME: don't know why this doesn't work #if self.settings.mainWindowFullScreen: # self._do_pending_fullscreen = True # timeline toolbar # FIXME: remove toolbar padding and shadow. In fullscreen mode, the # toolbar buttons should be clickable with the mouse cursor at the # very bottom of the screen. ttb = self.uimanager.get_widget("/TimelineToolBar") vbox.pack_start(ttb, expand=False) ttb.show() self.show() if not self.settings.mainWindowShowMainToolbar: self.toolbar.props.visible = False if not self.settings.mainWindowShowTimelineToolbar: ttb.props.visible = False #application icon self.set_icon_name("pitivi") #pulseaudio 'role' (http://0pointer.de/blog/projects/tagging-audio.htm os.environ["PULSE_PROP_media.role"] = "production" os.environ["PULSE_PROP_application.icon_name"] = "pitivi" def _connectToSourceList(self): self.sourcelist.connect('play', self._sourceListPlayCb) def toggleFullScreen(self): """ Toggle the fullscreen mode of the application """ if not self.is_fullscreen: self.viewer.window.fullscreen() self.is_fullscreen = True else: self.viewer.window.unfullscreen() self.is_fullscreen = False #TODO check if it is the way to go def setActionsSensitive(self, action_names, sensitive): """ Permit to get the focus for the keyboard letter keys for other operation as typing text in an entry, or loose it @param action_names: The name of actions we want to set to sensitive or not @type action_names: A {list} of action names @param sensitiive: %True if actions must be sensitive False otherwise @type action_names: C{Bool} """ for action in self.actiongroup.list_actions(): if action.get_name() in action_names: action.set_sensitive(sensitive) if self.timeline: for action_group in self.timeline.ui_manager.get_action_groups(): for action in action_group.list_actions(): if action.get_name() in action_names: action.set_sensitive(sensitive) ## PlayGround callback def _windowizeViewer(self, button, pane): # FIXME: the viewer can't seem to handle being unparented/reparented pane.remove(self.viewer) window = gtk.Window() window.add(self.viewer) window.connect("destroy", self._reparentViewer, pane) window.resize(200, 200) window.show_all() def _reparentViewer(self, window, pane): window.remove(self.viewer) pane.pack2(self.viewer, resize=False, shrink=False) self.viewer.show() ## Missing Plugin Support def _installPlugins(self, details, missingPluginsCallback): context = gst.pbutils.InstallPluginsContext() context.set_xid(self.window.xid) res = gst.pbutils.install_plugins_async(details, context, missingPluginsCallback) return res ## UI Callbacks def _configureCb(self, unused_widget, event): if not self.is_fullscreen: self.settings.mainWindowWidth = event.width self.settings.mainWindowHeight = event.height def _deleteCb(self, unused_widget, unused_data=None): self._saveWindowSettings() if not self.app.shutdown(): return True return False def _exposeEventCb(self, unused_widget, event): if self._do_pending_fullscreen: self._fullScreenAlternateCb(None) self._do_pending_fullscreen = False def _saveWindowSettings(self): self.settings.mainWindowFullScreen = self.is_fullscreen self.settings.mainWindowHPanePosition = self.hpaned.get_position() self.settings.mainWindowMainHPanePosition = self.mainhpaned.get_position() self.settings.mainWindowVPanePosition = self.vpaned.get_position() mtb = self.actiongroup.get_action("ShowHideMainToolbar") ttb = self.actiongroup.get_action("ShowHideTimelineToolbar") self.settings.mainWindowShowMainToolbar = mtb.props.active self.settings.mainWindowShowTimelineToolbar = ttb.props.active def _sourceListPlayCb(self, sourcelist, factory): self._viewFactory(factory) ## Toolbar/Menu actions callback def _newProjectMenuCb(self, unused_action): self.app.projectManager.newBlankProject() def _openProjectCb(self, unused_action): chooser = gtk.FileChooserDialog(_("Open File..."), self, action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) chooser.set_icon_name("pitivi") chooser.set_select_multiple(False) chooser.set_current_folder(self.settings.lastProjectFolder) formats = formatter.list_formats() for format in formats: filt = gtk.FileFilter() filt.set_name(format[1]) for ext in format[2]: filt.add_pattern("*%s" % ext) chooser.add_filter(filt) default = gtk.FileFilter() default.set_name(_("All Supported Formats")) default.add_custom(gtk.FILE_FILTER_URI, supported) chooser.add_filter(default) response = chooser.run() self.settings.lastProjectFolder = chooser.get_current_folder() if response == gtk.RESPONSE_OK: uri = chooser.get_uri() uri = unquote(uri) self.app.projectManager.loadProject(uri) chooser.destroy() return True def _saveProjectCb(self, unused_action): if not self.project.uri: self._saveProjectAsCb(unused_action) else: self.app.projectManager.saveProject(self.project, overwrite=True) def _saveProjectAsCb(self, unused_action): uri = self._showSaveAsDialog(self.app.current) if uri is not None: return self.app.projectManager.saveProject(self.project, uri, overwrite=True) return False def _revertToSavedProjectCb(self, unused_action): return self.app.projectManager.revertToSavedProject() def _projectSettingsCb(self, unused_action): from projectsettings import ProjectSettingsDialog ProjectSettingsDialog(self, self.app.current).show() def _quitCb(self, unused_action): self._saveWindowSettings() self.app.shutdown() def _fullScreenCb(self, unused_action): self.toggleFullScreen() def _fullScreenAlternateCb(self, unused_action): self.actiongroup.get_action("FullScreen").activate() def _showHideMainToolBar(self, action): self.uimanager.get_widget("/MainToolBar").props.visible = \ action.props.active def _showHideTimelineToolbar(self, action): self.uimanager.get_widget("/TimelineToolBar").props.visible = \ action.props.active def _aboutResponseCb(self, dialog, unused_response): dialog.destroy() def _showWebsiteCb(self, dialog, uri): import webbrowser webbrowser.open_new(uri) def _aboutCb(self, unused_action): abt = gtk.AboutDialog() abt.set_name(APPNAME) abt.set_version("v%s" % pitivi_version) gtk.about_dialog_set_url_hook(self._showWebsiteCb) abt.set_website("http://www.pitivi.org/") authors = ["Edward Hervey <*****@*****.**>", "Alessandro Decina <*****@*****.**>", "Brandon Lewis <*****@*****.**> (UI)", "", _("Contributors:"), "Christophe Sauthier <*****@*****.**> (i18n)", "Laszlo Pandy <*****@*****.**> (UI)", "Ernst Persson <*****@*****.**>", "Richard Boulton <*****@*****.**>", "Thibaut Girka <*****@*****.**> (UI)", "Jeff Fortin <*****@*****.**> (UI)", "Johan Dahlin <*****@*****.**> (UI)", "Luca Della Santina <*****@*****.**>", "Thijs Vermeir <*****@*****.**>", "Sarath Lakshman <*****@*****.**>"] abt.set_authors(authors) abt.set_license(_("GNU Lesser General Public License\n" "See http://www.gnu.org/copyleft/lesser.html for more details")) abt.set_icon_name("pitivi") abt.set_logo_icon_name("pitivi") abt.connect("response", self._aboutResponseCb) abt.show() def _undoCb(self, action): self.app.action_log.undo() def _redoCb(self, action): self.app.action_log.redo() def _pluginManagerCb(self, unused_action): from pluginmanagerdialog import PluginManagerDialog PluginManagerDialog(self.app.plugin_manager) # Import from Webcam callback def _ImportWebcam(self,unused_action): from webcam_managerdialog import WebcamManagerDialog w = WebcamManagerDialog(self.app) w.show() # Capture network stream callback def _ImportNetstream(self,unused_action): from netstream_managerdialog import NetstreamManagerDialog NetstreamManagerDialog() # screencast callback def _Screencast(self,unused_action): from screencast_managerdialog import ScreencastManagerDialog ScreencastManagerDialog(self.app) ## Devices changed def _deviceChangeCb(self, probe, unused_device): if len(probe.getVideoSourceDevices()) < 1: self.webcam_button.set_sensitive(False) else: self.webcam_button.set_sensitive(True) def _hideChildWindow(self, window, event): window.hide() return True def _prefsCb(self, unused_action): if not self.prefsdialog: from pitivi.ui.prefs import PreferencesDialog self.prefsdialog = PreferencesDialog(self.app) self.prefsdialog.set_transient_for(self) self.prefsdialog.connect("delete-event", self._hideChildWindow) self.prefsdialog.show() def rewind(self, unused_action): pass def playPause(self, unused_action): self.viewer.togglePlayback() def pause(self, unused_action): self.viewer.pause() def fastForward(self, unused_action): pass def loop(self, unused_action): pass def _projectManagerNewProjectLoadedCb(self, projectManager, project): self.log("A NEW project is loaded, update the UI!") self.project = project self._connectToProjectSources(project.sources) can_render = project.timeline.duration > 0 self.render_button.set_sensitive(can_render) self._syncDoUndo(self.app.action_log) if project.timeline.duration != 0: self._setBestZoomRatio() else: self._zoom_duration_changed = True self.project.seeker.connect("seek", self._timelineSeekCb) # preliminary seek to ensure the project pipeline is configured self.project.seeker.seek(0) def _setBestZoomRatio(self): ruler_width = self.timeline.ruler.get_allocation()[2] timeline_duration = self.project.timeline.duration ideal_zoom_ratio = ruler_width / float(timeline_duration / gst.SECOND) nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio) Zoomable.setZoomLevel(nearest_zoom_level) def _projectManagerNewProjectLoadingCb(self, projectManager, uri): self.log("A NEW project is being loaded, deactivate UI") def _projectManagerSaveProjectFailedCb(self, projectManager, project, uri, exception): # FIXME: do something here self.error("failed to save project") def _projectManagerProjectSavedCb(self, projectManager, project, uri): self.app.action_log.checkpoint() self._syncDoUndo(self.app.action_log) if project.uri is None: project.uri = uri def _projectManagerClosingProjectCb(self, projectManager, project): if not project.hasUnsavedModifications(): return True if project.uri: save = gtk.STOCK_SAVE else: save = gtk.STOCK_SAVE_AS dialog = gtk.Dialog("", self, gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR, (_("Close without saving"), gtk.RESPONSE_REJECT, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, save, gtk.RESPONSE_YES)) dialog.set_icon_name("pitivi") dialog.set_resizable(False) dialog.set_has_separator(False) dialog.set_default_response(gtk.RESPONSE_YES) primary = gtk.Label() primary.set_line_wrap(True) primary.set_use_markup(True) primary.set_alignment(0, 0.5) message = _("Save changes to the current project before closing?") primary.set_markup("<span weight=\"bold\">" + message + "</span>") secondary = gtk.Label() secondary.set_line_wrap(True) secondary.set_use_markup(True) secondary.set_alignment(0, 0.5) secondary.props.label = _("If you don't save some of your " "changes will be lost") # put the text in a vbox vbox = gtk.VBox(False, 12) vbox.pack_start(primary, expand=True, fill=True) vbox.pack_start(secondary, expand=True, fill=True) # make the [[image] text] hbox image = gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) hbox = gtk.HBox(False, 12) hbox.pack_start(image, expand=False) hbox.pack_start(vbox, expand=True, fill=True) action_area = dialog.get_action_area() # FIXME: find out where this "6" comes from. It's needed to align our # hbox with the action area button box hbox.set_border_width(6) # stuff the hbox in the dialog content_area = dialog.get_content_area() content_area.pack_start(hbox, expand=True, fill=True) content_area.set_spacing(14) hbox.show_all() response = dialog.run() dialog.destroy() if response == gtk.RESPONSE_YES: if project.uri is not None: res = self.app.projectManager.saveProject(project, overwrite=True) else: res = self._saveProjectAsCb(None) elif response == gtk.RESPONSE_REJECT: res = True else: res = False return res def _projectManagerProjectClosedCb(self, projectManager, project): # we must disconnect from the project pipeline before it is released self._disconnectFromProjectSources(project.sources) self.viewer.setAction(None) self.viewer.setPipeline(None) project.seeker.disconnect_by_func(self._timelineSeekCb) return False def _projectManagerRevertingToSavedCb(self, projectManager, project): if project.hasUnsavedModifications(): dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_NONE, _("Do you want to reload current project?") ) dialog.set_icon_name("pitivi") dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_NO, gtk.STOCK_REVERT_TO_SAVED, gtk.RESPONSE_YES) dialog.set_title(_("Revert to saved project")) dialog.set_resizable(False) dialog.set_property("secondary-text", _("All unsaved changes will be lost.") ) dialog.set_default_response(gtk.RESPONSE_NO) response = dialog.run() dialog.destroy() if response <> gtk.RESPONSE_YES: return False return True def _projectManagerNewProjectFailedCb(self, projectManager, uri, exception): # ungrey UI dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("PiTiVi is unable to load file \"%s\"") % uri) dialog.set_icon_name("pitivi") dialog.set_title(_("Error Loading File")) dialog.set_property("secondary-text", str(exception)) dialog.run() dialog.destroy() self.set_sensitive(True) def _projectManagerMissingUriCb(self, instance, formatter, uri, factory): dialog = gtk.Dialog(_("Locate missing file..."), self, gtk.DIALOG_MODAL, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_icon_name("pitivi") dialog.set_border_width(12) dialog.get_content_area().set_spacing(6) text = _("The following file has moved, please tell PiTiVi where to find it.") + \ "\n\n" + beautify_factory(factory) + "\n" + \ "<b>%s</b>" % _("Duration:") + beautify_length(factory.duration) label = gtk.Label() label.set_markup(text) label.set_justify(gtk.JUSTIFY_CENTER) dialog.get_content_area().pack_start(label, False, False) label.show() chooser = gtk.FileChooserWidget(action=gtk.FILE_CHOOSER_ACTION_OPEN) chooser.set_select_multiple(False) chooser.set_current_folder(self.settings.lastProjectFolder) dialog.get_content_area().pack_start(chooser, True, True) chooser.show() dialog.set_size_request(640, 480) response = dialog.run() if response == gtk.RESPONSE_OK: self.log("User chose a URI to save project to") new = chooser.get_uri() if new: formatter.addMapping(uri, unquote(new)) else: self.log("User didn't choose a URI to save project to") # FIXME: not calling addMapping doesn't keep the formatter from # re-emitting the same signal. How do we get out of this # situation? pass dialog.destroy() def _connectToProjectSources(self, sourcelist): sourcelist.connect("missing-plugins", self._sourceListMissingPluginsCb) def _disconnectFromProjectSources(self, sourcelist): sourcelist.disconnect_by_func(self._sourceListMissingPluginsCb) def _actionLogCommit(self, action_log, stack, nested): if nested: return self._syncDoUndo(action_log) def _actionLogCleaned(self, action_log): self._syncDoUndo(action_log) def _actionLogUndo(self, action_log, stack): self._syncDoUndo(action_log) def _actionLogRedo(self, action_log, stack): self._syncDoUndo(action_log) def _syncDoUndo(self, action_log): undo_action = self.actiongroup.get_action("Undo") can_undo = bool(action_log.undo_stacks) undo_action.set_sensitive(can_undo) dirty = action_log.dirty() save_action = self.actiongroup.get_action("SaveProject") save_action.set_sensitive(dirty) if self.app.current.uri is not None: revert_action = self.actiongroup.get_action("RevertToSavedProject") revert_action.set_sensitive(dirty) self.app.current.setModificationState(dirty) redo_action = self.actiongroup.get_action("Redo") can_redo = bool(action_log.redo_stacks) redo_action.set_sensitive(can_redo) if self.project is not None: app_name = "%s" % (APPNAME) title = u"%s \u2014 %s" % (self.project.name, app_name) if dirty: title = "*" + title title = title.encode("utf8") self.set_title(title) ## PiTiVi current project callbacks def _setProject(self): if self.project: self.project_pipeline = self.project.pipeline self.project_timeline = self.project.timeline if self.timeline: self.timeline.project = self.project self.clipconfig.project = self.project self.app.timelineLogObserver.effect_properties_tracker.pipeline\ = self.project.pipeline project = receiver(_setProject) @handler(project, "settings-changed") def _settingsChangedCb(self, project, old, new): if self.viewer.action == self.project.view_action: self.viewer.setDisplayAspectRatio(float(new.videopar * new.videowidth) / float(new.videoheight)) def _sourceListMissingPluginsCb(self, project, uri, factory, details, descriptions, missingPluginsCallback): res = self._installPlugins(details, missingPluginsCallback) return res ## Current Project Pipeline def _setProjectPipeline(self): if self.project_pipeline: # connect to timeline self.project_pipeline.activatePositionListener() self._timelinePipelinePositionChangedCb(self.project_pipeline, 0) project_pipeline = receiver() @handler(project_pipeline, "error") def _pipelineErrorCb(self, unused_pipeline, error, detail): pass @handler(project_pipeline, "position") def _timelinePipelinePositionChangedCb(self, pipeline, position): self.timeline.timelinePositionChanged(position) self.timelinepos = position @handler(project_pipeline, "state-changed") def _timelinePipelineStateChangedCb(self, pipeline, state): self.timeline.stateChanged(state) ## Project Timeline (not to be confused with UI timeline) project_timeline = receiver() @handler(project_timeline, "duration-changed") def _timelineDurationChangedCb(self, timeline, duration): if duration > 0: sensitive = True if self._zoom_duration_changed: self._setBestZoomRatio() self._zoom_duration_changed = False else: sensitive = False self.render_button.set_sensitive(sensitive) ## other def _showSaveAsDialog(self, project): self.log("Save URI requested") chooser = gtk.FileChooserDialog(_("Save As..."), self, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) chooser.set_icon_name("pitivi") chooser.set_select_multiple(False) chooser.set_current_name(_("Untitled.xptv")) chooser.set_current_folder(self.settings.lastProjectFolder) chooser.props.do_overwrite_confirmation = True formats = formatter.list_formats() for format in formats: filt = gtk.FileFilter() filt.set_name(format[1]) for ext in format[2]: filt.add_pattern("*.%s" % ext) chooser.add_filter(filt) default = gtk.FileFilter() default.set_name(_("Detect Automatically")) default.add_pattern("*") chooser.add_filter(default) response = chooser.run() self.settings.lastProjectFolder = chooser.get_current_folder() if response == gtk.RESPONSE_OK: self.log("User chose a URI to save project to") # need to do this to work around bug in gst.uri_construct # which escapes all /'s in path! uri = "file://" + chooser.get_filename() format = chooser.get_filter().get_name() if format == _("Detect Automatically"): format = None self.log("uri:%s , format:%s", uri, format) ret = uri else: self.log("User didn't choose a URI to save project to") ret = None chooser.destroy() return ret def _viewerDndDataReceivedCb(self, unused_widget, context, unused_x, unused_y, selection, targetType, ctime): # FIXME : This should be handled by the main application who knows how # to switch between pipelines. self.info("context:%s, targetType:%s", context, targetType) if targetType == dnd.TYPE_URI_LIST: uri = selection.data.strip().split("\n")[0].strip() elif targetType == dnd.TYPE_PITIVI_FILESOURCE: uri = selection.data else: context.finish(False, False, ctime) return # Use factory from our source list if we have the given uri try: fact = self.project.sources.getUri(uri) except SourceListError: from pitivi.factories.file import FileSourceFactory fact = FileSourceFactory(uri) self._viewFactory(fact) context.finish(True, False, ctime) def _viewFactory(self, factory): # FIXME: we change the viewer pipeline unconditionally for now # we need a pipeline for playback pipeline = Pipeline() action = ViewAction() action.addProducers(factory) self.viewer.setPipeline(None) self.viewer.showSlider() # FIXME: why do I have to call viewer.setAction ? self.viewer.setAction(action) self.viewer.setPipeline(pipeline) self.viewer.play() def _timelineSeekCb(self, ruler, position, format): self.debug("position:%s", gst.TIME_ARGS (position)) if self.viewer.action != self.project.view_action: self.viewer.setPipeline(None) self.viewer.hideSlider() self.viewer.setAction(self.project.view_action) self.viewer.setPipeline(self.project.pipeline) # get the pipeline settings and set the DAR of the viewer sett = self.project.getSettings() self.viewer.setDisplayAspectRatio(float(sett.videopar * sett.videowidth) / float(sett.videoheight)) # everything above only needs to be done if the viewer isn't already # set to the pipeline. self.project.pipeline.pause() try: self.project.pipeline.seek(position, format) except: self.debug("Seeking failed")
class PropertyEditor(gtk.ScrolledWindow): __MODULES__ = {} def __init__(self, instance, *args, **kwargs): gtk.ScrolledWindow.__init__(self, *args, **kwargs) self.instance = instance self.timeline = instance.current.timeline self._createUi() self._selectionChangedCb(self.timeline) self._module_instances = {} self._default_editor = DefaultPropertyEditor() def _createUi(self): # basic initialization self.set_border_width(5) # scrolled window self.set_shadow_type(gtk.SHADOW_ETCHED_IN) self._no_objs = gtk.Viewport() self._no_objs.add(gtk.Label(_("No Objects Selected"))) self._contents = self._no_objs self.add(self._no_objs) ## Public API @classmethod def addModule(cls, core_class, widget_class): cls.__MODULES__[core_class] = widget_class @classmethod def delModule(cls, core_class): del cls.__MODULES__[core_class] ## Internal Methods def _get_widget_for_type(self, t): w = self._default_editor if t in self._module_instances: w = self._module_instances[t] elif t in self.__MODULES__: w = self.__MODULES__[t]() self._module_instances[t] = w return w def _set_contents(self, widget): if widget != self._contents: self.remove(self._contents) self._contents = widget self.add(widget) self.show_all() ## Instance Callbacks instance = receiver() @handler(instance, "new-project-loaded") def _newProjectLoading(self, unused_inst, project): self.timeline = project.timeline @handler(instance, "new-project-failed") def _newProjectFailed(self, unused_inst, unused_reason, unused_uri): self.timeline = None ## Timeline Callbacks timeline = receiver() @handler(timeline, "selection-changed") def _selectionChangedCb(self, timeline): if not self.timeline: return objs = self.timeline.getSelection() if objs: t = same((type(obj.factory) for obj in objs)) if t: widget = self._get_widget_for_type(t) else: widget = DefaultPropertyEditor(objs) widget.setObjects(objs) else: widget = self._no_objs self._set_contents(widget)
class Curve(goocanvas.ItemSimple, goocanvas.Item, View, Zoomable): __gtype_name__ = 'Curve' class Controller(Controller): _cursor = HAND _kf = None def drag_start(self, item, target, event): self._view.app.action_log.begin("volume change") initial = self.from_item_event(item, event) if self._kf: self._mousedown = self._view.keyframes[self._kf] - initial if not self._kf: # we are moving the entire curve, so we need to know the # inital position of each keyframe self._offsets = dict(self._view.keyframes) self._segment = self._view.findSegment( self.xyToTimeValue(initial)[0]) def drag_end(self, item, target, event): self._view.app.action_log.commit() def set_pos(self, obj, pos): interpolator = self._view.interpolator if self._kf: time, value = self.xyToTimeValue(pos) self._kf.time = time self._kf.value = value else: for kf in self._segment: time, value = self.xyToTimeValue(self._offsets[kf] + pos - self.pos(self._view)) kf.value = value def hover(self, item, target, event): coords = self.from_item_event(item, event) self._kf = self._view.findKeyframe(coords) self._view.setFocusedKf(self._kf) def double_click(self, pos): interpolator = self._view.interpolator kf = self._view.findKeyframe(pos) if kf is None: time, value = self.xyToTimeValue(pos) self._view.app.action_log.begin("add volume point") kf = interpolator.newKeyframe(time) self._view.setFocusedKf(kf) self._view.app.action_log.commit() else: self._view.app.action_log.begin("remove volume point") self._view.interpolator.removeKeyframe(kf) self._view.app.action_log.commit() def xyToTimeValue(self, pos): view = self._view interpolator = view.interpolator bounds = view.bounds time = (Zoomable.pixelToNs(pos[0] - bounds.x1) + view.element.in_point) value = ((1 - (pos[1] - bounds.y1 - view._min) / view._range) * interpolator.range) + interpolator.lower return time, value def enter(self, item, target): coords = self.from_item_event(item, self._last_event) self._kf = self._view.findKeyframe(coords) self._view.setFocusedKf(self._kf) self._view.focus() def leave(self, item, target): self._view.normal() def __init__(self, instance, element, interpolator, height=LAYER_HEIGHT_EXPANDED, **kwargs): super(Curve, self).__init__(**kwargs) View.__init__(self) Zoomable.__init__(self) self.app = instance self.keyframes = {} self.height = float(height) self.element = element self.props.pointer_events = goocanvas.EVENTS_STROKE self.interpolator = interpolator self._focused_kf = None self.normal() ## properties def _get_height(self): return self._height def _set_height(self, value): self._height = value self._min = CURVE_STROKE_WIDTH / 2 self._max = value - (CURVE_STROKE_WIDTH / 2) self._range = self._max - self._min self.changed(True) height = gobject.property(_get_height, _set_height, type=float) ## element callbacks def _set_element(self): self.previewer = previewer.get_preview_for_object(self.element) element = receiver(setter=_set_element) @handler(element, "in-point-changed") @handler(element, "media-duration-changed") def _media_props_changed(self, obj, unused_start_duration): self.changed(True) ## interpolator callbacks interpolator = receiver() @handler(interpolator, "keyframe-removed") def keyframeRemoved(self, unused_interpolator, keyframe): if keyframe in self.keyframes: del self.keyframes[keyframe] if keyframe is self._focused_kf: self._focused_kf = None self.changed(False) @handler(interpolator, "keyframe-added") @handler(interpolator, "keyframe-moved") def curveChanged(self, unused_interpolator, unused_keyframe): self.changed(False) ## Zoomable interface overries def zoomChanged(self): self.changed(True) ## goocanvas item methods def do_simple_update(self, cr): cr.identity_matrix() if self.element.factory: self.bounds = goocanvas.Bounds( 0, 0, Zoomable.nsToPixel(self.element.duration), self.height) def _getKeyframeXY(self, kf): interp = self.interpolator x = self.nsToPixel(kf.time - self.element.in_point) y = self._range - (( (kf.value - interp.lower) / interp.range) * self._range) return point.Point(x + self.bounds.x1, y + self.bounds.y1 + self._min) def _controlPoint(self, cr, kf): pos = self._getKeyframeXY(kf) x, y = pos cr.rectangle(x - KW_WIDTH2, y - KW_HEIGHT2, KW_WIDTH, KW_HEIGHT) self.keyframes[kf] = pos def do_simple_paint(self, cr, bounds): bounds = intersect(self.bounds, bounds) cr.identity_matrix() if self.interpolator: height = bounds.y2 - bounds.y1 width = bounds.x2 - bounds.x1 cr.rectangle(bounds.x1, bounds.y1, width, height) cr.clip() self.make_curve(cr) cr.set_line_width(self.line_width) cr.set_source_rgb(1, 0, 0) cr.stroke() self.make_keyframes(cr) cr.set_line_width(1.0) cr.set_source_rgb(1, 1, 1) cr.fill_preserve() cr.set_source_rgb(1, 0, 0) cr.stroke() # re-draw the focused keyframe, if it exists, inverted if self._focused_kf: self._controlPoint(cr, self._focused_kf) cr.set_source_rgb(1, 0, 0) cr.fill_preserve() cr.set_source_rgb(1, 1, 1) cr.stroke() def make_curve(self, cr): if not self.interpolator: return iterator = self.interpolator.keyframes cr.move_to(*self._getKeyframeXY(iterator.next())) for kf in iterator: cr.line_to(*self._getKeyframeXY(kf)) cr.line_to(*self._getKeyframeXY(self.interpolator.end)) def make_keyframes(self, cr): for kf in self.interpolator.keyframes: self._controlPoint(cr, kf) def do_simple_is_item_at(self, x, y, cr, pointer_event): if (between(0, x, self.nsToPixel(self.element.duration)) and between(0, y, self.height)): x += self.bounds.x1 y += self.bounds.y1 self.make_curve(cr) cr.set_line_width(10.0) return cr.in_stroke(x, y) or bool(self.findKeyframe((x, y))) return False ## public def setFocusedKf(self, kf): if self._focused_kf is not kf: self.changed(False) self._focused_kf = kf def focus(self): self.line_width = CURVE_STROKE_WIDTH * 1.5 self.changed(False) def normal(self): self.line_width = CURVE_STROKE_WIDTH self.setFocusedKf(None) self.changed(False) def findKeyframe(self, pos): x, y = pos for keyframe, value in self.keyframes.iteritems(): kx, ky = value if (between(kx - KW_MOUSE_WIDTH, x, kx + KW_MOUSE_WIDTH) and between(ky - KW_MOUSE_HEIGHT, y, ky + KW_MOUSE_HEIGHT)): return keyframe return None def findSegment(self, time): before = self.interpolator.start after = self.interpolator.end for keyframe in self.keyframes.iterkeys(): if between(before.time, keyframe.time, time): before = keyframe if between(time, keyframe.time, after.time): after = keyframe assert before.time <= after.time return before, after
class Timeline(gtk.Table, Loggable, Zoomable): # the screen width of the current unit unit_width = 10 # specific levels of zoom, in (multiplier, unit) pairs which # from zoomed out to zoomed in def __init__(self, instance, ui_manager): gtk.Table.__init__(self, rows=2, columns=1, homogeneous=False) Loggable.__init__(self) Zoomable.__init__(self) self.log("Creating Timeline") self.project = None self.ui_manager = ui_manager self.app = instance self._temp_objects = None self._factories = None self._finish_drag = False self._position = 0 self._state = gst.STATE_NULL self._createUI() self._prev_duration = 0 self.shrink = True self.rate = gst.Fraction(1, 1) self._seeker = Seeker(80) self._seeker.connect('seek', self._seekerSeekCb) def _createUI(self): self.leftSizeGroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) self.hadj = gtk.Adjustment() self.vadj = gtk.Adjustment() # controls for tracks and layers self._controls = TimelineControls() controlwindow = gtk.ScrolledWindow(None, self.vadj) controlwindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER) controlwindow.add_with_viewport(self._controls) controlwindow.set_size_request(-1, 1) self.attach(controlwindow, 0, 1, 1, 2, xoptions=0) # timeline ruler self.ruler = ruler.ScaleRuler(self.hadj) self.ruler.set_size_request(0, 25) self.ruler.set_border_width(2) self.ruler.connect("key-press-event", self._keyPressEventCb) self.attach(self.ruler, 1, 2, 0, 1, yoptions=0) # proportional timeline self._canvas = TimelineCanvas(self.app) timelinewindow = gtk.ScrolledWindow(self.hadj, self.vadj) timelinewindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) timelinewindow.add(self._canvas) #FIXME: remove padding between scrollbar and scrolled window self.attach(timelinewindow, 1, 2, 1, 2) # drag and drop self.drag_dest_set(gtk.DEST_DEFAULT_MOTION, [dnd.FILESOURCE_TUPLE], gtk.gdk.ACTION_COPY) self.connect("drag-data-received", self._dragDataReceivedCb) self.connect("drag-leave", self._dragLeaveCb) self.connect("drag-drop", self._dragDropCb) self.connect("drag-motion", self._dragMotionCb) # toolbar actions actions = ( ("ZoomIn", gtk.STOCK_ZOOM_IN, None, None, ZOOM_IN, self._zoomInCb), ("ZoomOut", gtk.STOCK_ZOOM_OUT, None, None, ZOOM_OUT, self._zoomOutCb), ) selection_actions = ( ("DeleteObj", gtk.STOCK_DELETE, None, "Delete", DELETE, self.deleteSelected), ("UnlinkObj", "pitivi-unlink", None, "<Shift><Control>L", UNLINK, self.unlinkSelected), ("LinkObj", "pitivi-link", None, "<Control>L", LINK, self.linkSelected), ("UngroupObj", "pitivi-ungroup", None, "<Shift><Control>G", UNGROUP, self.ungroupSelected), ("GroupObj", "pitivi-group", None, "<Control>G", GROUP, self.groupSelected), ) toggle_actions = (("Razor", "pitivi-split", _("Razor"), "<Ctrl>R", RAZOR, self.toggleRazor), ) actiongroup = gtk.ActionGroup("timelinepermanent") actiongroup.add_actions(actions) actiongroup.add_toggle_actions(toggle_actions) self.ui_manager.insert_action_group(actiongroup, 0) actiongroup = gtk.ActionGroup("timelineselection") actiongroup.add_actions(selection_actions) self.link_action = actiongroup.get_action("LinkObj") self.unlink_action = actiongroup.get_action("UnlinkObj") self.group_action = actiongroup.get_action("GroupObj") self.ungroup_action = actiongroup.get_action("UngroupObj") self.delete_action = actiongroup.get_action("DeleteObj") self.ui_manager.insert_action_group(actiongroup, -1) self.ui_manager.add_ui_from_string(ui) # drag and drop self.drag_dest_set(gtk.DEST_DEFAULT_MOTION, [dnd.FILESOURCE_TUPLE], gtk.gdk.ACTION_COPY) self.connect("drag-data-received", self._dragDataReceivedCb) self.connect("drag-leave", self._dragLeaveCb) self.connect("drag-drop", self._dragDropCb) self.connect("drag-motion", self._dragMotionCb) self._canvas.connect("button-press-event", self._buttonPress) self._canvas.connect("button-release-event", self._buttonRelease) self._canvas.connect("key-press-event", self._keyPressEventCb) ## Event callbacks def _keyPressEventCb(self, unused_widget, event): kv = event.keyval self.debug("kv:%r", kv) if kv not in [gtk.keysyms.Left, gtk.keysyms.Right]: return False mod = event.get_state() try: if mod & gtk.gdk.CONTROL_MASK: now = self.project.pipeline.getPosition() ltime, rtime = self.project.timeline.edges.closest(now) if kv == gtk.keysyms.Left: if mod & gtk.gdk.SHIFT_MASK: self._seekRelative(-gst.SECOND) elif mod & gtk.gdk.CONTROL_MASK: self._seeker.seek(ltime + 1) else: self._seekRelative(-long(self.rate * gst.SECOND)) elif kv == gtk.keysyms.Right: if mod & gtk.gdk.SHIFT_MASK: self._seekRelative(gst.SECOND) elif mod & gtk.gdk.CONTROL_MASK: self._seeker.seek(rtime + 1) else: self._seekRelative(long(self.rate * gst.SECOND)) finally: return True def _seekRelative(self, time): pipeline = self.project.pipeline seekvalue = max( 0, min(pipeline.getPosition() + time, pipeline.getDuration())) self._seeker.seek(seekvalue) def _seekerSeekCb(self, seeker, position, format): self.project.pipeline.seek(position, format) def _buttonPress(self, window, event): self.shrink = False def _buttonRelease(self, window, event): self.shrink = True self._timelineStartDurationChanged(self.timeline, self.timeline.duration) ## Drag and Drop callbacks def _dragMotionCb(self, unused, context, x, y, timestamp): self.warning("self._factories:%r, self._temp_objects:%r", not not self._factories, not not self._temp_objects) if self._factories is None: atom = gtk.gdk.atom_intern(dnd.FILESOURCE_TUPLE[0]) self.drag_get_data(context, atom, timestamp) self.drag_highlight() else: # actual drag-and-drop if not self._temp_objects: self.timeline.disableUpdates() self._add_temp_source() focus = self._temp_objects[0] self._move_context = MoveContext(self.timeline, focus, set(self._temp_objects[1:])) self._move_temp_source(self.hadj.props.value + x, y) return True def _dragLeaveCb(self, unused_layout, unused_context, unused_tstamp): if self._temp_objects: try: for obj in self._temp_objects: self.timeline.removeTimelineObject(obj, deep=True) finally: self._temp_objects = None self.drag_unhighlight() self.timeline.enableUpdates() def _dragDropCb(self, widget, context, x, y, timestamp): self.app.action_log.begin("add clip") self._add_temp_source() focus = self._temp_objects[0] self._move_context = MoveContext(self.timeline, focus, set(self._temp_objects[1:])) self._move_temp_source(self.hadj.props.value + x, y) self._move_context.finish() self.app.action_log.commit() context.drop_finish(True, timestamp) self._factories = None self._temp_objects = None return True def _dragDataReceivedCb(self, unused_layout, context, x, y, selection, targetType, timestamp): self.log("SimpleTimeline, targetType:%d, selection.data:%s" % (targetType, selection.data)) # FIXME: let's have just one target type, call it # TYPE_PITIVI_OBJECTFACTORY. # TODO: handle uri targets by doign an import-add. This would look # something like this: # tell current project to import the uri # wait for source-added signal, meanwhile ignore dragMotion signals # when ready, add factories to the timeline. if targetType == dnd.TYPE_PITIVI_FILESOURCE: uris = selection.data.split("\n") else: context.finish(False, False, timestamp) self._factories = [self.project.sources.getUri(uri) for uri in uris] context.drag_status(gtk.gdk.ACTION_COPY, timestamp) return True def _add_temp_source(self): self._temp_objects = [ self.timeline.addSourceFactory(factory) for factory in self._factories ] def _move_temp_source(self, x, y): x1, y1, x2, y2 = self._controls.get_allocation() offset = 10 + (x2 - x1) x, y = self._canvas.convert_from_pixels(x - offset, y) priority = int((y // (LAYER_HEIGHT_EXPANDED + LAYER_SPACING))) delta = Zoomable.pixelToNs(x) self._move_context.editTo(delta, priority) ## Zooming and Scrolling def zoomChanged(self): # this has to be in a timeout, because the resize hasn't actually # completed yet, and so the canvas can't actually complete the scroll gobject.idle_add(self.scrollToPlayhead) def timelinePositionChanged(self, position): self._position = position self.ruler.timelinePositionChanged(position) self._canvas._position = position if self._state == gst.STATE_PLAYING: self.scrollToPlayhead() def stateChanged(self, state): self._state = state def scrollToPlayhead(self): width = self.get_allocation().width new_pos = Zoomable.nsToPixel(self._position) scroll_pos = self.hadj.get_value() if (new_pos < scroll_pos) or (new_pos > scroll_pos + width): self.scrollToPosition(new_pos - width / 2) return False def scrollToPosition(self, position): if position > self.hadj.upper: # we can't perform the scroll because the canvas needs to be # updated gobject.idle_add(self._scrollToPosition, position) else: self._scrollToPosition(position) def _scrollToPosition(self, position): self.hadj.set_value(position) return False ## Project callbacks def _setProject(self): if self.project: self.timeline = self.project.timeline self._controls.timeline = self.timeline self._canvas.timeline = self.timeline self._canvas.zoomChanged() self.ruler.setProjectFrameRate( self.project.getSettings().videorate) self.ruler.zoomChanged() self._settingsChangedCb(self.project) project = receiver(_setProject) @handler(project, "settings-changed") def _settingsChangedCb(self, project): rate = self.project.getSettings().videorate self.rate = float(1 / rate) self.ruler.setProjectFrameRate(rate) ## Timeline callbacks def _setTimeline(self): if self.timeline: self._timelineSelectionChanged(self.timeline) self._timelineStartDurationChanged(self.timeline, self.timeline.duration) self._controls.timeline = self.timeline timeline = receiver(_setTimeline) @handler(timeline, "duration-changed") def _timelineStartDurationChanged(self, unused_timeline, duration): if self.shrink: self._prev_duration = duration self.ruler.setMaxDuration(duration + 60 * gst.SECOND) self._canvas.setMaxDuration(duration + 60 * gst.SECOND) self.ruler.setShadedDuration(duration) else: # only resize if new size is larger if duration > self._prev_duration: self._prev_duration = duration self.ruler.setMaxDuration(duration) self._canvas.setMaxDuration(duration) #self.ruler.setShadedDuration(duration) @handler(timeline, "selection-changed") def _timelineSelectionChanged(self, timeline): delete = False link = False unlink = False group = False ungroup = False timeline_objects = {} if timeline.selection: delete = True if len(timeline.selection) > 1: link = True group = True start = None duration = None for obj in self.timeline.selection: if obj.link: unlink = True if len(obj.track_objects) > 1: ungroup = True if start is not None and duration is not None: if obj.start != start or obj.duration != duration: group = False else: start = obj.start duration = obj.duration self.delete_action.set_sensitive(delete) self.link_action.set_sensitive(link) self.unlink_action.set_sensitive(unlink) self.group_action.set_sensitive(group) self.ungroup_action.set_sensitive(ungroup) ## ToolBar callbacks def hide(self): self.actiongroup.set_visible(False) gtk.Vbox.hide(self) def _zoomInCb(self, unused_action): Zoomable.zoomIn() def _zoomOutCb(self, unused_action): Zoomable.zoomOut() def deleteSelected(self, unused_action): if self.timeline: self.app.action_log.begin("delete clip") self.timeline.deleteSelection() self.app.action_log.commit() def unlinkSelected(self, unused_action): if self.timeline: self.timeline.unlinkSelection() def linkSelected(self, unused_action): if self.timeline: self.timeline.linkSelection() def ungroupSelected(self, unused_action): if self.timeline: self.timeline.ungroupSelection() def groupSelected(self, unused_action): if self.timeline: self.timeline.groupSelection() def toggleRazor(self, action): if action.props.active: self._canvas.activateRazor(action) else: self._canvas.deactivateRazor()
class Preview(goocanvas.ItemSimple, goocanvas.Item, Zoomable): __gtype_name__ = 'Preview' def __init__(self, instance, element, height=46, **kwargs): super(Preview, self).__init__(**kwargs) Zoomable.__init__(self) self.app = instance self.height = float(height) self.element = element self.props.pointer_events = False # ghetto hack self.hadj = instance.gui.timeline.hadj ## properties def _get_height(self): return self._height def _set_height(self, value): self._height = value self.changed(True) height = gobject.property(_get_height, _set_height, type=float) ## element callbacks def _set_element(self): self.previewer = previewer.get_preview_for_object(self.app, self.element) element = receiver(setter=_set_element) @handler(element, "in-point-changed") @handler(element, "media-duration-changed") def _media_props_changed(self, obj, unused_start_duration): self.changed(True) ## previewer callbacks previewer = receiver() @handler(previewer, "update") def _update_preview(self, previewer, segment): # if segment is none we are not just drawing a new thumbnail, so we # should update bounds if segment == None: self.changed(True) else: self.changed(False) ## Zoomable interface overries def zoomChanged(self): self.changed(True) ## goocanvas item methods def do_simple_update(self, cr): cr.identity_matrix() if self.element.factory: border_width = self.previewer._spacing() self.bounds = goocanvas.Bounds(border_width, 4, max(0, Zoomable.nsToPixel(self.element.duration) - border_width), self.height) def do_simple_paint(self, cr, bounds): x1 = -self.hadj.get_value() cr.identity_matrix() if self.element.factory: self.previewer.render_cairo(cr, intersect(self.bounds, bounds), self.element, x1, self.bounds.y1) def do_simple_is_item_at(self, x, y, cr, pointer_event): return (between(0, x, self.nsToPixel(self.element.duration)) and between(0, y, self.height))
class TimelineObject(View, goocanvas.Group, Zoomable): element = receiver() __HEIGHT__ = 50 __NORMAL__ = 0x709fb899 __SELECTED__ = 0xa6cee3AA class Controller(TimelineController): def click(self, pos): mode = 0 if self._last_event.get_state() & gtk.gdk.SHIFT_MASK: mode = 1 elif self._last_event.get_state() & gtk.gdk.CONTROL_MASK: mode = 2 self._view.timeline.setSelectionToObj(self._view.element, mode) def __init__(self, element, composition, timeline): goocanvas.Group.__init__(self) View.__init__(self) Zoomable.__init__(self) self.element = element self.comp = composition self.timeline = timeline self.bg = goocanvas.Rect(height=self.__HEIGHT__, fill_color_rgba=self.__NORMAL__, line_width=0) self.content = Preview(self.element) self.name = goocanvas.Text(x=10, text=os.path.basename( unquote(element.factory.name)), font="Sans 9", fill_color_rgba=0x000000FF, alignment=pango.ALIGN_LEFT) self.start_handle = StartHandle(element, timeline, height=self.__HEIGHT__) self.end_handle = EndHandle(element, timeline, height=self.__HEIGHT__) for thing in (self.bg, self.content, self.start_handle, self.end_handle, self.name): self.add_child(thing) if element: self.zoomChanged() self.normal() def zoomChanged(self): self._start_duration_cb(self.element, self.element.start, self.element.duration) @handler(element, "start-duration-changed") def _start_duration_cb(self, obj, start, duration): self.set_simple_transform(self.nsToPixel(start), 0, 1, 0) width = self.nsToPixel(duration) w = width - self.end_handle.props.width self.name.props.clip_path = "M%g,%g h%g v%g h-%g z" % ( 0, 0, w, self.__HEIGHT__, w) self.bg.props.width = width # place end handle at appropriate distance self.end_handle.props.x = w @handler(element, "selected-changed") def _selected_changed(self, element): if element.selected: self.bg.props.fill_color_rgba = self.__SELECTED__ else: self.bg.props.fill_color_rgba = self.__NORMAL__
class Preview(goocanvas.ItemSimple, goocanvas.Item, Zoomable): __gtype_name__ = 'Preview' def __init__(self, element, height=50, **kwargs): super(Preview, self).__init__(**kwargs) Zoomable.__init__(self) self.height = float(height) self.element = element self.props.pointer_events = False ## properties def _get_height(self): return self._height def _set_height(self, value): self._height = value self.changed(True) height = gobject.property(_get_height, _set_height, type=float) ## element callbacks def _set_element(self): self.previewer = previewer.get_preview_for_object(self.element) element = receiver(setter=_set_element) @handler(element, "in-point-changed") @handler(element, "media-duration-changed") def _media_props_changed(self, obj, unused_start_duration): self.changed(True) ## previewer callbacks previewer = receiver() @handler(previewer, "update") def _update_preview(self, previewer, segment): self.changed(False) ## Zoomable interface overries def zoomChanged(self): self.changed(True) ## goocanvas item methods def do_simple_update(self, cr): cr.identity_matrix() if self.element.factory: self.bounds = goocanvas.Bounds( 0, 0, Zoomable.nsToPixel(self.element.duration), self.height) def do_simple_paint(self, cr, bounds): cr.identity_matrix() if self.element.factory: self.previewer.render_cairo(cr, intersect(self.bounds, bounds), self.element, self.bounds.y1) def do_simple_is_item_at(self, x, y, cr, pointer_event): return (between(0, x, self.nsToPixel(self.element.duration)) and between(0, y, self.height))
class Timeline(gtk.Table, Loggable, Zoomable): # the screen width of the current unit unit_width = 10 # specific levels of zoom, in (multiplier, unit) pairs which # from zoomed out to zoomed in def __init__(self, instance, ui_manager): gtk.Table.__init__(self, rows=2, columns=1, homogeneous=False) Loggable.__init__(self) Zoomable.__init__(self) self.log("Creating Timeline") self._updateZoom = True self.project = None self.ui_manager = ui_manager self.app = instance self._temp_objects = None self._factories = None self._finish_drag = False self._position = 0 self._state = gst.STATE_NULL self._createUI() self._prev_duration = 0 self.shrink = True self.rate = gst.Fraction(1,1) def _createUI(self): self.leftSizeGroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) self.props.row_spacing = 2 self.props.column_spacing = 2 self.hadj = gtk.Adjustment() self.vadj = gtk.Adjustment() # zooming slider self._zoomAdjustment = gtk.Adjustment() self._zoomAdjustment.set_value(Zoomable.getCurrentZoomLevel()) self._zoomAdjustment.connect("value-changed", self._zoomAdjustmentChangedCb) self._zoomAdjustment.props.lower = 0 self._zoomAdjustment.props.upper = Zoomable.zoom_steps zoomslider = gtk.HScale(self._zoomAdjustment) zoomslider.props.draw_value = False zoomslider.set_tooltip_text(_("Zoom Timeline")) self.attach(zoomslider, 0, 1, 0, 1, yoptions=0, xoptions=gtk.FILL) # controls for tracks and layers self._controls = TimelineControls() controlwindow = gtk.Viewport(None, self.vadj) controlwindow.add(self._controls) controlwindow.set_size_request(-1, 1) controlwindow.set_shadow_type(gtk.SHADOW_OUT) self.attach(controlwindow, 0, 1, 1, 2, xoptions=0) # timeline ruler self.ruler = ruler.ScaleRuler(self.app, self.hadj) self.ruler.set_size_request(0, 25) self.ruler.set_border_width(2) self.ruler.connect("key-press-event", self._keyPressEventCb) self.ruler.connect("size-allocate", self._rulerSizeAllocateCb) rulerframe = gtk.Frame() rulerframe.set_shadow_type(gtk.SHADOW_OUT) rulerframe.add(self.ruler) self.attach(rulerframe, 1, 2, 0, 1, yoptions=0) # proportional timeline self._canvas = TimelineCanvas(self.app) self._root_item = self._canvas.get_root_item() self.attach(self._canvas, 1, 2, 1, 2) # scrollbar self._hscrollbar = gtk.HScrollbar(self.hadj) self._vscrollbar = gtk.VScrollbar(self.vadj) self.attach(self._hscrollbar, 1, 2, 2, 3, yoptions=0) self.attach(self._vscrollbar, 2, 3, 1, 2, xoptions=0) self.hadj.connect("value-changed", self._updateScrollPosition) self.vadj.connect("value-changed", self._updateScrollPosition) # error infostub self.infostub = InfoStub() self.attach(self.infostub, 1, 2, 4, 5, yoptions=0) self.show_all() self.infostub.hide() # toolbar actions actions = ( ("ZoomIn", gtk.STOCK_ZOOM_IN, None, "<Control>plus", ZOOM_IN, self._zoomInCb), ("ZoomOut", gtk.STOCK_ZOOM_OUT, None, "<Control>minus", ZOOM_OUT, self._zoomOutCb), # actions for adding additional accelerators ("ControlEqualAccel", gtk.STOCK_ZOOM_IN, None, "<Control>equal", ZOOM_IN, self._zoomInCb), ("ControlKPAddAccel", gtk.STOCK_ZOOM_IN, None, "<Control>KP_Add", ZOOM_IN, self._zoomInCb), ("ControlKPSubtractAccel", gtk.STOCK_ZOOM_OUT, None, "<Control>KP_Subtract", ZOOM_OUT, self._zoomOutCb), ) selection_actions = ( ("DeleteObj", gtk.STOCK_DELETE, None, "Delete", DELETE, self.deleteSelected), ("UnlinkObj", "pitivi-unlink", None, "<Shift><Control>L", UNLINK, self.unlinkSelected), ("LinkObj", "pitivi-link", None, "<Control>L", LINK, self.linkSelected), ("UngroupObj", "pitivi-ungroup", None, "<Shift><Control>G", UNGROUP, self.ungroupSelected), ("GroupObj", "pitivi-group", None, "<Control>G", GROUP, self.groupSelected), ) self.playhead_actions = ( ("Split", "pitivi-split", _("Split"), "S", SPLIT, self.split), ("Keyframe", "pitivi-keyframe", _("Add a keyframe"), "K", KEYFRAME, self.keyframe), ("Prevframe", "pitivi-prevframe", _("_Prevframe"), "E", PREVFRAME, self.prevframe), ("Nextframe", "pitivi-nextframe", _("_Nextframe"), "R", NEXTFRAME, self.nextframe), ) actiongroup = gtk.ActionGroup("timelinepermanent") actiongroup.add_actions(actions) self.ui_manager.insert_action_group(actiongroup, 0) actiongroup = gtk.ActionGroup("timelineselection") actiongroup.add_actions(selection_actions) actiongroup.add_actions(self.playhead_actions) self.link_action = actiongroup.get_action("LinkObj") self.unlink_action = actiongroup.get_action("UnlinkObj") self.group_action = actiongroup.get_action("GroupObj") self.ungroup_action = actiongroup.get_action("UngroupObj") self.delete_action = actiongroup.get_action("DeleteObj") self.split_action = actiongroup.get_action("Split") self.keyframe_action = actiongroup.get_action("Keyframe") self.prevframe_action = actiongroup.get_action("Prevframe") self.nextframe_action = actiongroup.get_action("Nextframe") self.ui_manager.insert_action_group(actiongroup, -1) self.ui_manager.add_ui_from_string(ui) # drag and drop self.drag_dest_set(gtk.DEST_DEFAULT_MOTION, [dnd.FILESOURCE_TUPLE, dnd.EFFECT_TUPLE], gtk.gdk.ACTION_COPY) self.connect("drag-data-received", self._dragDataReceivedCb) self.connect("drag-leave", self._dragLeaveCb) self.connect("drag-drop", self._dragDropCb) self.connect("drag-motion", self._dragMotionCb) self._canvas.connect("button-press-event", self._buttonPress) self._canvas.connect("button-release-event", self._buttonRelease) self._canvas.connect("key-press-event", self._keyPressEventCb) ## Event callbacks def _keyPressEventCb(self, unused_widget, event): kv = event.keyval self.debug("kv:%r", kv) if kv not in [gtk.keysyms.Left, gtk.keysyms.Right]: return False mod = event.get_state() try: if mod & gtk.gdk.CONTROL_MASK: now = self.project.pipeline.getPosition() ltime, rtime = self.project.timeline.edges.closest(now) if kv == gtk.keysyms.Left: if mod & gtk.gdk.SHIFT_MASK: self._seekRelative(-gst.SECOND) elif mod & gtk.gdk.CONTROL_MASK: self._seeker.seek(ltime+1) else: self._seekRelative(-long(self.rate * gst.SECOND)) elif kv == gtk.keysyms.Right: if mod & gtk.gdk.SHIFT_MASK: self._seekRelative(gst.SECOND) elif mod & gtk.gdk.CONTROL_MASK: self._seeker.seek(rtime+1) else: self._seekRelative(long(self.rate * gst.SECOND)) finally: return True def _seekRelative(self, time): pipeline = self.project.pipeline seekvalue = max(0, min(pipeline.getPosition() + time, pipeline.getDuration())) self._seeker.seek(seekvalue) def _buttonPress(self, window, event): self.shrink = False def _buttonRelease(self, window, event): self.shrink = True self._timelineStartDurationChanged(self.timeline, self.timeline.duration) ## Drag and Drop callbacks def _dragMotionCb(self, unused, context, x, y, timestamp): self.warning("self._factories:%r, self._temp_objects:%r", not not self._factories, not not self._temp_objects) if self._factories is None: if context.targets in DND_EFFECT_LIST: atom = gtk.gdk.atom_intern(dnd.EFFECT_TUPLE[0]) else: atom = gtk.gdk.atom_intern(dnd.FILESOURCE_TUPLE[0]) self.drag_get_data(context, atom, timestamp) self.drag_highlight() else: if context.targets not in DND_EFFECT_LIST: if not self._temp_objects: self.timeline.disableUpdates() self._add_temp_source() focus = self._temp_objects[0] self._move_context = MoveContext(self.timeline, focus, set(self._temp_objects[1:])) self._move_temp_source(self.hadj.props.value + x, y) return True def _dragLeaveCb(self, unused_layout, context, unused_tstamp): if self._temp_objects: try: for obj in self._temp_objects: self.timeline.removeTimelineObject(obj, deep=True) finally: self._temp_objects = None self.drag_unhighlight() self.timeline.enableUpdates() def _dragDropCb(self, widget, context, x, y, timestamp): if context.targets not in DND_EFFECT_LIST: self.app.action_log.begin("add clip") self.timeline.disableUpdates() self._add_temp_source() self.timeline.selection.setSelection(self._temp_objects, SELECT) focus = self._temp_objects[0] self._move_context = MoveContext(self.timeline, focus, set(self._temp_objects[1:])) self._move_temp_source(self.hadj.props.value + x, y) self._move_context.finish() self.app.action_log.commit() context.drop_finish(True, timestamp) self._factories = None self._temp_objects = None self.app.current.seeker.seek(self._position) return True elif context.targets in DND_EFFECT_LIST: if not self.timeline.timeline_objects: return False factory = self._factories[0] timeline_objs = self._getTimelineObjectUnderMouse(x, y, factory.getInputStreams()[0]) if timeline_objs: self.app.action_log.begin("add effect") self.timeline.addEffectFactoryOnObject(factory, timeline_objects = timeline_objs) self.app.action_log.commit() self._factories = None self.app.current.seeker.seek(self._position) context.drop_finish(True, timestamp) self.timeline.selection.setSelection(timeline_objs, SELECT) return True return False def _dragDataReceivedCb(self, unused_layout, context, x, y, selection, targetType, timestamp): self.log("SimpleTimeline, targetType:%d, selection.data:%s" % (targetType, selection.data)) # FIXME: let's have just one target type, call it # TYPE_PITIVI_OBJECTFACTORY. # TODO: handle uri targets by doign an import-add. This would look # something like this: # tell current project to import the uri # wait for source-added signal, meanwhile ignore dragMotion signals # when ready, add factories to the timeline. if targetType not in [dnd.TYPE_PITIVI_FILESOURCE, dnd.TYPE_PITIVI_EFFECT]: context.finish(False, False, timestamp) return if targetType == dnd.TYPE_PITIVI_FILESOURCE: uris = selection.data.split("\n") self._factories = [self.project.sources.getUri(uri) for uri in uris] else: if not self.timeline.timeline_objects: return False self._factories = [self.app.effects.getFactoryFromName(selection.data)] context.drag_status(gtk.gdk.ACTION_COPY, timestamp) return True def _getTimelineObjectUnderMouse(self, x, y, stream): timeline_objs = [] items_in_area = self._canvas.getItemsInArea(x, y-15, x+1, y-30) tracks = [obj for obj in items_in_area[0]] track_objects = [obj for obj in items_in_area[1]] for track_object in track_objects: if (type(stream) == type(track_object.stream)): timeline_objs.append(track_object.timeline_object) return timeline_objs def _add_temp_source(self): self._temp_objects = [self.timeline.addSourceFactory(factory) for factory in self._factories] def _move_temp_source(self, x, y): x1, y1, x2, y2 = self._controls.get_allocation() offset = 10 + (x2 - x1) x, y = self._canvas.convert_from_pixels(x - offset, y) priority = int((y // (LAYER_HEIGHT_EXPANDED + LAYER_SPACING))) delta = Zoomable.pixelToNs(x) self._move_context.editTo(delta, priority) ## Zooming and Scrolling def _updateScrollPosition(self, adjustment): self._root_item.set_simple_transform( -self.hadj.get_value(), -self.vadj.get_value(), 1.0, 0) def _zoomAdjustmentChangedCb(self, adjustment): # GTK crack self._updateZoom = False Zoomable.setZoomLevel(int(adjustment.get_value())) self._updateZoom = True def zoomChanged(self): self._canvas.props.redraw_when_scrolled = True if self._updateZoom: self._zoomAdjustment.set_value(self.getCurrentZoomLevel()) self.ruler.queue_resize() self.ruler.queue_draw() def timelinePositionChanged(self, position): self._position = position self.ruler.timelinePositionChanged(position) self._canvas.timelinePositionChanged(position) if self._state == gst.STATE_PLAYING: self.scrollToPlayhead() def stateChanged(self, state): self._state = state def scrollToPlayhead(self): """ If the current position is out of the view bouds, then scroll as close to the center of the view as possible or as close as the timeline canvas allows. """ page_size = self.hadj.get_page_size() new_pos = Zoomable.nsToPixel(self._position) scroll_pos = self.hadj.get_value() if (new_pos > scroll_pos + page_size) or (new_pos < scroll_pos): self.scrollToPosition(min(new_pos - page_size / 2, self.hadj.upper - page_size - 1)) return False def scrollToPosition(self, position): if position > self.hadj.upper: # we can't perform the scroll because the canvas needs to be # updated gobject.idle_add(self._scrollToPosition, position) else: self._scrollToPosition(position) def _scrollToPosition(self, position): self.hadj.set_value(position) return False def _rulerSizeAllocateCb(self, ruler, allocation): self._canvas.props.redraw_when_scrolled = False ## Project callbacks def _setProject(self): if self.project: self.timeline = self.project.timeline self._controls.timeline = self.timeline self._canvas.timeline = self.timeline self._canvas.zoomChanged() self.ruler.setProjectFrameRate(self.project.getSettings().videorate) self.ruler.zoomChanged() self._settingsChangedCb(self.project, None, self.project.getSettings()) self._seeker = self.project.seeker project = receiver(_setProject) @handler(project, "settings-changed") def _settingsChangedCb(self, project, old, new): rate = new.videorate self.rate = float(1 / rate) self.ruler.setProjectFrameRate(rate) ## Timeline callbacks def _setTimeline(self): if self.timeline: self._timelineSelectionChanged(self.timeline) self._timelineStartDurationChanged(self.timeline, self.timeline.duration) self._controls.timeline = self.timeline timeline = receiver(_setTimeline) @handler(timeline, "duration-changed") def _timelineStartDurationChanged(self, unused_timeline, duration): if self.shrink: self._prev_duration = duration self.ruler.setMaxDuration(duration + 60 * gst.SECOND) self._canvas.setMaxDuration(duration + 60 * gst.SECOND) self.ruler.setShadedDuration(duration) else: # only resize if new size is larger if duration > self._prev_duration: self._prev_duration = duration self.ruler.setMaxDuration(duration) self._canvas.setMaxDuration(duration) #self.ruler.setShadedDuration(duration) @handler(timeline, "selection-changed") def _timelineSelectionChanged(self, timeline): delete = False link = False unlink = False group = False ungroup = False split = False keyframe = False timeline_objects = {} if timeline.selection: delete = True if len(timeline.selection) > 1: link = True group = True start = None duration = None for obj in self.timeline.selection: if obj.link: link = False unlink = True if len(obj.track_objects) > 1: ungroup = True if start is not None and duration is not None: if obj.start != start or obj.duration != duration: group = False else: start = obj.start duration = obj.duration split = True keyframe = True self.delete_action.set_sensitive(delete) self.link_action.set_sensitive(link) self.unlink_action.set_sensitive(unlink) self.group_action.set_sensitive(group) self.ungroup_action.set_sensitive(ungroup) self.split_action.set_sensitive(split) self.keyframe_action.set_sensitive(keyframe) ## ToolBar callbacks def hide(self): self.actiongroup.set_visible(False) gtk.Vbox.hide(self) def _zoomInCb(self, unused_action): Zoomable.zoomIn() def _zoomOutCb(self, unused_action): Zoomable.zoomOut() def deleteSelected(self, unused_action): if self.timeline: self.app.action_log.begin("delete clip") self.timeline.deleteSelection() self.app.action_log.commit() def unlinkSelected(self, unused_action): if self.timeline: self.timeline.unlinkSelection() def linkSelected(self, unused_action): if self.timeline: self.timeline.linkSelection() def ungroupSelected(self, unused_action): if self.timeline: self.app.action_log.begin("ungroup") self.timeline.ungroupSelection() self.app.action_log.commit() def groupSelected(self, unused_action): if self.timeline: self.timeline.groupSelection() def split(self, action): self.app.action_log.begin("split") self.timeline.disableUpdates() self.timeline.split(self._position) self.timeline.enableUpdates() self.app.action_log.commit() # work-around for 603149 self.project.seeker.seek(self._position) def keyframe(self, action): timeline_position = self._position selected = self.timeline.selection.getSelectedTrackObjs() for obj in selected: keyframe_exists = False position_in_obj = (timeline_position - obj.start) + obj.in_point interpolators = obj.getInterpolators() for value in interpolators: interpolator = obj.getInterpolator(value) keyframes = interpolator.getInteriorKeyframes() for kf in keyframes: if kf.getTime() == position_in_obj: keyframe_exists = True self.app.action_log.begin("remove volume point") interpolator.removeKeyframe(kf) self.app.action_log.commit() if keyframe_exists == False: self.app.action_log.begin("add volume point") interpolator.newKeyframe(position_in_obj) self.app.action_log.commit() def prevframe(self, action): timeline_position = self._position prev_kf = self.timeline.getPrevKeyframe(timeline_position) if prev_kf != None: self._seeker.seek(prev_kf) self.scrollToPlayhead() def nextframe(self, action): timeline_position = self._position next_kf = self.timeline.getNextKeyframe(timeline_position) if next_kf: self._seeker.seek(next_kf) self.scrollToPlayhead()
class Controller(object): """A controller which implements drag-and-drop bahavior on connected view objects. Subclasses may override the drag_start, drag_end, pos, and set_pos methods""" # note we SHOULD be using the gtk function for this, but it doesn't appear # to be exposed in pygtk __DRAG_THRESHOLD__ = Point(0, 0) _view = receiver() _dragging = None _canvas = None _cursor = None _ptr_within = False _last_click = None _initial = None _mousedown = None _last_event = None _pending_drag_start = None _pending_drag_end = False _shift_down = False _control_down = False _handle_enter_leave = True _handle_mouse_up_down = True _handle_motion_notify = True def __init__(self, view=None): object.__init__(self) self._view = view ## convenience functions def from_event(self, event): """returns the coordinates of an event""" return Point(*self._canvas.convert_from_pixels(event.x, event.y)) def from_item_event(self, item, event): return Point(*self._canvas.convert_from_item_space( item, *self.from_event(event))) def to_item_space(self, item, point): return Point(*self._canvas.convert_to_item_space(item, *point)) def pos(self, item): bounds = item.get_bounds() return Point(bounds.x1, bounds.y1) ## signal handlers @handler(_view, "enter_notify_event") def enter_notify_event(self, item, target, event): self._event_common(item, target, event) self._canvas.grab_focus(item) if self._cursor and item is target: event.window.set_cursor(self._cursor) if not self._dragging: self.enter(item, target) self._ptr_within = True return self._handle_enter_leave or self._dragging @handler(_view, "leave_notify_event") def leave_notify_event(self, item, target, event): self._event_common(item, target, event) self._canvas.keyboard_ungrab(item, event.time) self._ptr_within = False if not self._dragging: self.leave(item, target) event.window.set_cursor(ARROW) return self._handle_enter_leave or self._dragging @handler(_view, "button_press_event") def button_press_event(self, item, target, event): self._event_common(item, target, event) if not self._canvas: self._canvas = item.get_canvas() self._mousedown = self.pos(item) - self.transform( self.from_item_event(item, event)) self._dragging = target self._initial = self.pos(target) self._pending_drag_start = (item, target, event) return self._handle_mouse_up_down @handler(_view, "motion_notify_event") def motion_notify_event(self, item, target, event): self._event_common(item, target, event) if self._dragging: if self._pending_drag_start is not None: pending_drag_start, self._pending_drag_start = \ self._pending_drag_start, None self._pending_drag_end = True self._drag_start(*pending_drag_start) self.set_pos( self._dragging, self.transform(self._mousedown + self.from_item_event(item, event))) return self._handle_motion_notify else: self.hover(item, target, event) return False @handler(_view, "button_release_event") def button_release_event(self, item, target, event): self._event_common(item, target, event) self._drag_end(item, self._dragging, event) self._dragging = None return self._handle_mouse_up_down @handler(_view, "key_press_event") def key_press_event(self, item, target, event): self._event_common(item, target, event) kv = event.keyval if kv in (gtk.keysyms.Shift_L, gtk.keysyms.Shift_R): self._shift_down = True elif kv in (gtk.keysyms.Control_L, gtk.keysyms.Control_R): self._control_down = True return self.key_press(kv) @handler(_view, "key_release_event") def key_release_event(self, item, target, event): self._event_common(item, target, event) kv = event.keyval if kv in (gtk.keysyms.Shift_L, gtk.keysyms.Shift_R): self._shift_down = False elif kv in (gtk.keysyms.Control_L, gtk.keysyms.Control_R): self._control_down = False return self.key_release(kv) ## internal callbacks def _event_common(self, item, target, event): if not self._canvas: self._canvas = item.get_canvas() self._last_event = event s = event.get_state() self._shift_down = s & gtk.gdk.SHIFT_MASK self._control_down = s & gtk.gdk.CONTROL_MASK def _drag_start(self, item, target, event): self.drag_start(item, target, event) def _drag_end(self, item, target, event): self._pending_drag_start = None pending_drag_end, self._pending_drag_end = self._pending_drag_end, False if pending_drag_end: self.drag_end(item, target, event) if self._ptr_within and self._drag_threshold(): point = self.from_item_event(item, event) if self._last_click and (event.time - self._last_click < 400): self.double_click(point) else: self.click(point) self._last_click = event.time event.window.set_cursor(self._cursor) else: event.window.set_cursor(ARROW) def _drag_threshold(self): last = self.pos(self._dragging) difference = abs(self._initial - last) if abs(self._initial - last) > self.__DRAG_THRESHOLD__: return False return True ## protected interface for subclasses def click(self, pos): pass def double_click(self, pos): pass def drag_start(self, item, target, event): pass def drag_end(self, item, target, event): pass def set_pos(self, obj, pos): obj.props.x, obj.props.y = pos def transform(self, pos): return pos def enter(self, item, target): pass def leave(self, item, target): pass def key_press(self, keyval): pass def key_release(self, keyval): pass def hover(self, item, target, event): pass
class TimelineCanvas(goocanvas.Canvas, Zoomable, Loggable): __gsignals__ = { "scroll-event":"override" } _tracks = None def __init__(self, instance, timeline=None): goocanvas.Canvas.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.app = instance self._selected_sources = [] self._tracks = [] self._height = 0 self._position = 0 self._block_size_request = False self.props.integer_layout = True self.props.automatic_bounds = False self._createUI() self.timeline = timeline self.settings = instance.settings def _createUI(self): self._cursor = ARROW root = self.get_root_item() self.tracks = goocanvas.Group() root.add_child(self.tracks) self._marquee = goocanvas.Rect( stroke_pattern = unpack_cairo_pattern(0x33CCFF66), fill_pattern = unpack_cairo_pattern(0x33CCFF66), visibility = goocanvas.ITEM_INVISIBLE) self._razor = goocanvas.Rect( line_width=0, fill_color="orange", width=1, visibility=goocanvas.ITEM_INVISIBLE) root.add_child(self._marquee) root.add_child(self._razor) root.connect("motion-notify-event", self._selectionDrag) root.connect("button-press-event", self._selectionStart) root.connect("button-release-event", self._selectionEnd) height = (LAYER_HEIGHT_EXPANDED + TRACK_SPACING + LAYER_SPACING) * 2 # add some padding for the horizontal scrollbar height += 21 self.set_size_request(-1, height) def from_event(self, event): return Point(*self.convert_from_pixels(event.x, event.y)) def setExpanded(self, track_object, expanded): track_ui = None for track in self._tracks: if track.track == track_object: track_ui = track break track_ui.setExpanded(expanded) def do_scroll_event(self, event): if event.state & gtk.gdk.SHIFT_MASK: # shift + scroll => vertical (up/down) scroll if event.direction == gtk.gdk.SCROLL_LEFT: event.direction = gtk.gdk.SCROLL_UP elif event.direction == gtk.gdk.SCROLL_RIGHT: event.direction = gtk.gdk.SCROLL_DOWN event.state &= ~gtk.gdk.SHIFT_MASK elif event.state & gtk.gdk.CONTROL_MASK: # zoom + scroll => zooming (up: zoom in) if event.direction == gtk.gdk.SCROLL_UP: Zoomable.zoomIn() return True elif event.direction == gtk.gdk.SCROLL_DOWN: Zoomable.zoomOut() return True return False else: if event.direction == gtk.gdk.SCROLL_UP: event.direction = gtk.gdk.SCROLL_LEFT elif event.direction == gtk.gdk.SCROLL_DOWN: event.direction = gtk.gdk.SCROLL_RIGHT return goocanvas.Canvas.do_scroll_event(self, event) ## sets the cursor as appropriate def _mouseEnterCb(self, unused_item, unused_target, event): event.window.set_cursor(self._cursor) return True ## implements selection marquee _selecting = False _mousedown = None _marquee = None def _normalize(self, p1, p2): w, h = p2 - p1 x, y = p1 if w < 0: w = abs(w) x -= w if h < 0: h = abs(h) y -= h return (x, y), (w, h) def _selectionDrag(self, item, target, event): if self._selecting: cur = self.from_event(event) pos, size = self._normalize(self._mousedown, cur) m = self._marquee m.props.x, m.props.y = pos m.props.width, m.props.height = size return True return False def _selectionStart(self, item, target, event): self._selecting = True self._marquee.props.visibility = goocanvas.ITEM_VISIBLE self._mousedown = self.from_event(event) self._marquee.props.width = 0 self._marquee.props.height = 0 self.pointer_grab(self.get_root_item(), gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_RELEASE_MASK, self._cursor, event.time) return True def _selectionEnd(self, item, target, event): self.pointer_ungrab(self.get_root_item(), event.time) self._selecting = False self._marquee.props.visibility = goocanvas.ITEM_INVISIBLE mode = 0 if event.get_state() & gtk.gdk.SHIFT_MASK: mode = 1 if event.get_state() & gtk.gdk.CONTROL_MASK: mode = 2 self.timeline.setSelectionTo(self._objectsUnderMarquee(), mode) return True def _objectsUnderMarquee(self): items = self.get_items_in_area(self._marquee.get_bounds(), True, True, True) if items: return set((item.element for item in items if isinstance(item, TrackObject))) return set() ## Razor Tool Implementation def activateRazor(self, action): self._razor_sigid = self.connect("button_press_event", self._razorClickedCb) self._razor_release_sigid = self.connect("button_release_event", self._razorReleasedCb) self._razor_motion_sigid = self.connect("motion_notify_event", self._razorMovedCb) self._razor.props.visibility = goocanvas.ITEM_VISIBLE self._action = action return True def deactivateRazor(self): self.disconnect(self._razor_sigid) self.disconnect(self._razor_motion_sigid) self.disconnect(self._razor_release_sigid) self._razor.props.visibility = goocanvas.ITEM_INVISIBLE def _razorMovedCb(self, canvas, event): def snap(x): pos = self.nsToPixel(self._position) if abs(x - pos) <= self.settings.edgeSnapDeadband: return pos return x x, y = self.convert_from_pixels(event.x, event.y) self._razor.props.x = snap(self.nsToPixel(self.pixelToNs(x))) return True def _razorReleasedCb(self, unused_canvas, event): self._action.props.active = False x, y = self.convert_from_pixels(event.x, event.y) bounds = goocanvas.Bounds(x, y, x, y) items = self.get_items_in_area(bounds, True, True, True) if items: for item in items: if isinstance(item, TrackObject): self.app.action_log.begin("split object") item.element.split(self._snapToPlayhead(self.pixelToNs(x))) self.app.action_log.commit() return True def _razorClickedCb(self, unused_canvas, unused_event): return True def _snapToPlayhead(self, time): thresh = self.pixelToNs(self.settings.edgeSnapDeadband) if abs(time - self._position) <= thresh: return self._position return time max_duration = 0 def setMaxDuration(self, duration): self.max_duration = duration self._request_size() def _request_size(self): w = Zoomable.nsToPixel(self.max_duration) self.set_bounds(0, 0, w, self._height) self._razor.props.height = self._height self.get_root_item().changed(True) ## Zoomable Override def zoomChanged(self): if self.timeline: self.timeline.dead_band = self.pixelToNs( self.settings.edgeSnapDeadband) self._request_size() ## settings callbacks def _setSettings(self): self.zoomChanged() settings = receiver(_setSettings) @handler(settings, "edgeSnapDeadbandChanged") def _edgeSnapDeadbandChangedCb(self, settings): self.zoomChanged() ## Timeline callbacks def _set_timeline(self): while self._tracks: self._trackRemoved(None, 0) if self.timeline: for track in self.timeline.tracks: self._trackAdded(None, track) self.zoomChanged() timeline = receiver(_set_timeline) @handler(timeline, "track-added") def _trackAdded(self, timeline, track): track = Track(self.app, track, self.timeline) self._tracks.append(track) track.set_canvas(self) self.tracks.add_child(track) self.regroupTracks() @handler(timeline, "track-removed") def _trackRemoved(self, unused_timeline, position): track = self._tracks[position] del self._tracks[position] track.remove() self.regroupTracks() def regroupTracks(self): height = 0 for i, track in enumerate(self._tracks): track.set_simple_transform(0, height, 1, 0) height += track.height + TRACK_SPACING self._height = height self._request_size()
class TimelineCanvas(goocanvas.Canvas, Zoomable, Loggable): __gtype_name__ = 'TimelineCanvas' __gsignals__ = { "expose-event": "override", } _tracks = None def __init__(self, instance, timeline=None): goocanvas.Canvas.__init__(self) Zoomable.__init__(self) Loggable.__init__(self) self.app = instance self._selected_sources = [] self._tracks = [] self.height = 0 self._position = 0 self._block_size_request = False self.props.integer_layout = True self.props.automatic_bounds = False self.props.clear_background = False self.get_root_item().set_simple_transform(0, 2.0, 1.0, 0) self._createUI() self.timeline = timeline self.settings = instance.settings def _createUI(self): self._cursor = ARROW root = self.get_root_item() self.tracks = goocanvas.Group() self.tracks.set_simple_transform(0, KW_LABEL_Y_OVERFLOW, 1.0, 0) root.add_child(self.tracks) self._marquee = goocanvas.Rect( parent=root, stroke_pattern=unpack_cairo_pattern(0x33CCFF66), fill_pattern=unpack_cairo_pattern(0x33CCFF66), visibility=goocanvas.ITEM_INVISIBLE) self._playhead = goocanvas.Rect(y=-10, parent=root, line_width=1, fill_color_rgba=0x000000FF, stroke_color_rgba=0xFFFFFFFF, width=3) self._playhead_controller = PlayheadController(self._playhead) self.connect("size-allocate", self._size_allocate_cb) root.connect("motion-notify-event", self._selectionDrag) root.connect("button-press-event", self._selectionStart) root.connect("button-release-event", self._selectionEnd) self.height = (LAYER_HEIGHT_EXPANDED + TRACK_SPACING + LAYER_SPACING) * 2 # add some padding for the horizontal scrollbar self.height += 21 self.set_size_request(-1, self.height) def from_event(self, event): x, y = event.x, event.y x += self.app.gui.timeline.hadj.get_value() return Point(*self.convert_from_pixels(x, y)) def setExpanded(self, track_object, expanded): track_ui = None for track in self._tracks: if track.track == track_object: track_ui = track break track_ui.setExpanded(expanded) ## sets the cursor as appropriate def _mouseEnterCb(self, unused_item, unused_target, event): event.window.set_cursor(self._cursor) return True def do_expose_event(self, event): allocation = self.get_allocation() width = allocation.width height = allocation.height # draw the canvas background # we must have props.clear_background set to False self.style.apply_default_background(event.window, True, gtk.STATE_ACTIVE, event.area, event.area.x, event.area.y, event.area.width, event.area.height) goocanvas.Canvas.do_expose_event(self, event) ## implements selection marquee _selecting = False _mousedown = None _marquee = None _got_motion_notify = False def getItemsInArea(self, x1, y1, x2, y2): ''' Permits to get the Non UI L{Track}/L{TrackObject} in a list of set corresponding to the L{Track}/L{TrackObject} which are in the are @param x1: The horizontal coordinate of the up left corner of the area @type x1: An C{int} @param y1: The vertical coordinate of the up left corner of the area @type y1: An C{int} @param x2: The horizontal coordinate of the down right corner of the area @type x2: An C{int} @param x2: The vertical coordinate of the down right corner of the area @type x2: An C{int} @returns: A list of L{Track}, L{TrackObject} tuples ''' items = self.get_items_in_area(goocanvas.Bounds(x1, y1, x2, y2), True, True, True) if not items: return [], [] tracks = set() track_objects = set() for item in items: if isinstance(item, Track): tracks.add(item.track) elif isinstance(item, TrackObject): track_objects.add(item.element) return tracks, track_objects def _normalize(self, p1, p2, adjust=0): w, h = p2 - p1 x, y = p1 if w - adjust < 0: w = abs(w - adjust) x -= w else: w -= adjust if h < 0: h = abs(h) y -= h return (x, y), (w, h) def _selectionDrag(self, item, target, event): if self._selecting: self._got_motion_notify = True cur = self.from_event(event) pos, size = self._normalize(self._mousedown, cur, self.app.gui.timeline.hadj.get_value()) self._marquee.props.x, self._marquee.props.y = pos self._marquee.props.width, self._marquee.props.height = size return True return False def _selectionStart(self, item, target, event): self._selecting = True self._marquee.props.visibility = goocanvas.ITEM_VISIBLE self._mousedown = self.from_event(event) self._marquee.props.width = 0 self._marquee.props.height = 0 self.pointer_grab( self.get_root_item(), gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_RELEASE_MASK, self._cursor, event.time) return True def _selectionEnd(self, item, target, event): seeker = self.app.current.seeker self.pointer_ungrab(self.get_root_item(), event.time) self._selecting = False self._marquee.props.visibility = goocanvas.ITEM_INVISIBLE if not self._got_motion_notify: self.timeline.setSelectionTo(set(), 0) seeker.seek(Zoomable.pixelToNs(event.x)) else: self._got_motion_notify = False mode = 0 if event.get_state() & gtk.gdk.SHIFT_MASK: mode = 1 if event.get_state() & gtk.gdk.CONTROL_MASK: mode = 2 self.timeline.setSelectionTo(self._objectsUnderMarquee(), mode) return True def _objectsUnderMarquee(self): items = self.get_items_in_area(self._marquee.get_bounds(), True, True, True) if items: return set((item.element for item in items if isinstance(item, TrackObject) and item.bg in items)) return set() ## playhead implementation position = 0 def timelinePositionChanged(self, position): self.position = position self._playhead.props.x = self.nsToPixel(position) max_duration = 0 def setMaxDuration(self, duration): self.max_duration = duration self._request_size() def _request_size(self): alloc = self.get_allocation() self.set_bounds(0, 0, alloc.width, alloc.height) self._playhead.props.height = (self.height + SPACING) def _size_allocate_cb(self, widget, allocation): self._request_size() def zoomChanged(self): self.queue_draw() if self.timeline: self.timeline.dead_band = self.pixelToNs( self.settings.edgeSnapDeadband) self.timelinePositionChanged(self.position) ## settings callbacks def _setSettings(self): self.zoomChanged() settings = receiver(_setSettings) @handler(settings, "edgeSnapDeadbandChanged") def _edgeSnapDeadbandChangedCb(self, settings): self.zoomChanged() ## Timeline callbacks def _set_timeline(self): while self._tracks: self._trackRemoved(None, 0) if self.timeline: for track in self.timeline.tracks: self._trackAdded(None, track) self.zoomChanged() timeline = receiver(_set_timeline) @handler(timeline, "track-added") def _trackAdded(self, timeline, track): track = Track(self.app, track, self.timeline) self._tracks.append(track) track.set_canvas(self) self.tracks.add_child(track) self.regroupTracks() @handler(timeline, "track-removed") def _trackRemoved(self, unused_timeline, position): track = self._tracks[position] del self._tracks[position] track.remove() self.regroupTracks() def regroupTracks(self): height = 0 for i, track in enumerate(self._tracks): track.set_simple_transform(0, height, 1, 0) height += track.height + TRACK_SPACING self.height = height self._request_size()