# # THIS FILE IS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL. SEE # THE 'COPYING' FILE FOR DETAILS # # RecordingView.py # # A sub-class of gtk.Frame containing the visual layout of instrument # tracks, timeline, and horizontal scrollbars. # #------------------------------------------------------------------------------- import gtk import InstrumentViewer import TimeLineBar import Globals import ui.MessageArea as MessageArea from gettext import gettext as _ import urllib #========================================================================= class RecordingView(gtk.Frame): """ This class encapsulates a visual layout of a project comprising instrument tracks, timeline, and horizontal scrollbars. """ """ GTK widget name """ __gtype_name__ = 'RecordingView' """ How far in you are allowed to zoom. """ ZOOM_MAX_SCALE = 100.0 """ How for out you are allowed to zoom. This is just a default value and will change depending on the project length.""" ZOOM_MIN_SCALE = 5.0 """ Number only to be used inside Jokosher """ URI_DRAG_TYPE = 86 """ Custom numbers for use while dragging text in Jokosher """ DRAG_TARGETS = [ ( "text/uri-list", # Accept uri-lists 0, # From everywhere URI_DRAG_TYPE ), # Use the custom number ("text/plain", 0, URI_DRAG_TYPE) # so drags from Firefox work ] """The number of seconds shown after the end of the last event""" EXTRA_SCROLL_TIME = 25 #_____________________________________________________________________ def __init__(self, project, mainview, small=False): """ Creates a new instance of RecordingView. Parameters: project -- the currently active Project. mainview -- the main Jokosher window (MainApp). small -- set to True if we want small edit views (i.e. for the mixing view). """ gtk.Frame.__init__(self) self.project = project self.mainview = mainview self.small = small self.timelinebar = TimeLineBar.TimeLineBar(self.project, mainview) self.errorMessageArea = None self.restoreMessageArea = None ## create darker workspace box self.eventBox = gtk.EventBox() self.eventBox.connect("button-press-event", self.OnEmptySpaceDoubleClicked) self.vbox = gtk.VBox() self.add(self.vbox) self.vbox.pack_start(self.timelinebar, False, False) self.instrumentWindow = gtk.ScrolledWindow() self.instrumentBox = gtk.VBox() ## pack the instrument box inside the eventbox as you cant modify the color of a gtk.Box self.eventBox.add(self.instrumentBox) self.instrumentWindow.add_with_viewport(self.eventBox) self.instrumentWindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.vbox.pack_start(self.instrumentWindow, True, True) self.instrumentWindow.child.set_shadow_type(gtk.SHADOW_NONE) self.views = [] self.header_size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) self.header_size_group.add_widget(self.timelinebar.GetHeaderWidget()) self.hb = gtk.HBox() self.hb.set_spacing(6) self.hb.set_border_width(6) self.vbox.pack_end(self.hb, False, False) self.vbox.pack_end(gtk.HSeparator(), False, False) self.zoom_hb = gtk.HBox() self.zoom_hb.set_spacing(6) self.zoom_hb.set_border_width(0) self.header_size_group.add_widget(self.zoom_hb) self.scrollRange = gtk.Adjustment() self.scrollBar = gtk.HScrollbar(self.scrollRange) self.hb.pack_start(self.zoom_hb, False, False) self.hb.pack_start(self.scrollBar, True, True) self.lastzoom = 0 self.zoomSlider = gtk.HScale() self.zoomSlider.set_size_request(70, -1) self.zoomSlider.set_range(self.ZOOM_MIN_SCALE, self.ZOOM_MAX_SCALE) self.zoomSlider.set_increments(0.2, 0.2) self.zoomSlider.set_draw_value(False) self.zoomSlider.set_value(self.project.viewScale) self.zoomSlider.set_tooltip_text(_("Zoom the timeline - Right-Click to reset to the default level")) self.zoomSlider.connect("value-changed", self.OnZoom) self.zoomSlider.connect("button-press-event", self.OnZoomReset) self.inbutton = gtk.Button() inimg = gtk.image_new_from_stock(gtk.STOCK_ZOOM_IN, gtk.ICON_SIZE_BUTTON) self.inbutton.set_image(inimg) self.inbutton.set_relief(gtk.RELIEF_NONE) self.inbutton.set_tooltip_text(_("Zoom in timeline")) self.inbutton.connect("clicked", self.OnZoomIn) self.outbutton = gtk.Button() outimg = gtk.image_new_from_stock(gtk.STOCK_ZOOM_OUT, gtk.ICON_SIZE_BUTTON) self.outbutton.set_image(outimg) self.outbutton.set_relief(gtk.RELIEF_NONE) self.outbutton.set_tooltip_text(_("Zoom out timeline")) self.outbutton.connect("clicked", self.OnZoomOut) self.zoom_hb.pack_start( self.outbutton, False, False) self.zoom_hb.pack_start( self.zoomSlider, False, False) self.zoom_hb.pack_start( self.inbutton, False, False) self.centreViewOnPosition = False self.scrollRange.lower = 0 self.scrollRange.upper = 100 self.scrollRange.value = 0 self.scrollRange.step_increment = 1 self.scrollBar.connect("value-changed", self.OnScroll) self.connect("expose-event", self.OnExpose) self.connect("button_release_event", self.OnExpose) self.connect("button_press_event", self.OnMouseDown) #connect to the project signals self.project.connect("gst-bus-error", self.OnProjectGstError) self.project.connect("incremental-save", self.OnProjectIncSave) self.project.connect("instrument::added", self.OnInstrumentAdded) self.project.connect("instrument::reordered", self.OnInstrumentReordered) self.project.connect("instrument::removed", self.OnInstrumentRemoved) self.project.connect("view-start", self.OnViewStartChanged) self.vbox.drag_dest_set( gtk.DEST_DEFAULT_DROP, self.DRAG_TARGETS, gtk.gdk.ACTION_COPY) self.vbox.connect("drag_data_received", self.OnDragDataReceived) self.vbox.connect("drag_motion", self.OnDragMotion) #add the instruments that were loaded from the project file already for instr in self.project.instruments: self.OnInstrumentAdded(project, instr) self.show_all() self.show_all() if self.small: self.inbutton.hide() self.outbutton.hide() self.zoomSlider.hide() if self.project.CanDoIncrementalRestore(): message = _("Would you like to restore the current project?") details = _("A crash was detected and changes to your project were not saved.\nIf you would like, you can attempt to recover these lost changes.") msg_area = MessageArea.MessageArea() msg_area.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE) msg_area.add_stock_button_with_text(_("_Restore Project"), gtk.STOCK_APPLY, gtk.RESPONSE_OK) msg_area.set_text_and_icon(gtk.STOCK_DIALOG_QUESTION, message, details) msg_area.connect("response", self.OnRestoreMessageAreaResponse, msg_area) msg_area.connect("close", self.OnRestoreMessageAreaClose, msg_area) self.vbox.pack_end(msg_area, False, False) msg_area.show() self.restoreMessageArea = msg_area #_____________________________________________________________________ def OnExpose(self, widget, event): """ Sets scrollbar properties (i.e. size, scroll increments, etc), once space for the object has been allocated. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. event -- reserved for GTK callbacks, don't use it explicitly. """ st = self.rc_get_style().bg[gtk.STATE_NORMAL] ## Set the new color components st.red -= 3000 st.green -= 3000 st.blue -= 3000 cmap = self.get_colormap() ## allocate new colors col = cmap.alloc_color(st.red, st.green, st.blue) stcp = self.get_style().copy() stcp.bg[gtk.STATE_NORMAL] = col ## use the new colormap self.eventBox.set_style(stcp) # calculate scrollable width (scroll bar should always be same width as viewable area) self.scrollRange.page_size = (self.scrollBar.allocation.width) / self.project.viewScale self.scrollRange.page_increment = self.scrollRange.page_size # add EXTRA_SCROLL_TIME extra seconds length = self.project.GetProjectLength() + self.EXTRA_SCROLL_TIME self.scrollRange.upper = length if self.centreViewOnPosition: self.centreViewOnPosition = False #set the view to be centred over the playhead start = self.project.transport.GetPosition() - (self.scrollRange.page_size / 2) self.project.SetViewStart(start) # Need to adjust project view start if we are zooming out # and the end of the project is now before the end of the page. # Project end will be at right edge unless the start is also on # screen, in which case the start will be at the left. elif self.project.viewStart + self.scrollRange.page_size > length: self.project.SetViewStart(length - self.scrollRange.page_size) #check the min zoom value (based on project length) # (scroll bar should always be same width as viewable area) pixelSize = self.scrollBar.allocation.width minScale = pixelSize / length self.zoomSlider.set_range(minScale, self.ZOOM_MAX_SCALE) if self.zoomSlider.get_value() < minScale: self.zoomSlider.set_value(minScale) #_____________________________________________________________________ def OnInstrumentAdded(self, project, instrument): """ Callback for when an instrument is added to the project. Parameters: project -- The project that the instrument was added to. instrument -- The instrument that was added. """ instrViewer = InstrumentViewer.InstrumentViewer(project, instrument, self, self.mainview, self.small) #Add it to the views self.views.append((instrument.id, instrViewer)) self.header_size_group.add_widget(instrViewer.GetHeaderWidget()) self.instrumentBox.pack_start(instrViewer, False, False) instrViewer.show() #_____________________________________________________________________ def OnInstrumentRemoved(self, project, instrument): """ Callback for when an instrument is removed from the project. Parameters: project -- The project that the instrument was removed from. instrument -- The instrument that was removed. """ for ID, instrViewer in self.views: if ID == instrument.id: if instrViewer.parent: self.instrumentBox.remove(instrViewer) instrViewer.Destroy() self.views.remove((ID, instrViewer)) break #_____________________________________________________________________ def OnInstrumentReordered(self, project, instrument): """ Callback for when an instrument's position in the project has changed. Parameters: project -- The project that the instrument was changed on. instrument -- The instrument that was reordered. """ for ID, instrViewer in self.views: if ID == instrument.id: if instrViewer.parent: pos = self.project.instruments.index(instrument) self.instrumentBox.reorder_child(instrViewer, pos) instrViewer.show_all() break #_____________________________________________________________________ def OnProjectGstError(self, project, error, debug): """ Callback for when the project sends a gstreamer error message from the pipeline. Parameters: project -- The project instance that send the signal. error -- The type of error that occurred as a string. debug -- A string with more debug information about the error. """ if not error: error = _("A Gstreamer error has occurred") if not self.errorMessageArea: msg_area = self.CreateDefaultErrorPane(error, debug) self.vbox.pack_end(msg_area, False, False) msg_area.show() self.errorMessageArea = msg_area #_____________________________________________________________________ def CreateDefaultErrorPane(self, error, details): message = _("A GStreamer error has occurred.") info = _("If this problem persists consider reporting a bug using the link in the help menu.") details = "\n".join((error, info)) msg_area = MessageArea.MessageArea() msg_area.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE) msg_area.set_text_and_icon(gtk.STOCK_DIALOG_ERROR, message, details) msg_area.connect("response", self.OnMessageAreaReponse, msg_area) msg_area.connect("close", self.OnMessageAreaClose, msg_area) return msg_area #_____________________________________________________________________ def OnMessageAreaClose(self, widget, message_area): if self.errorMessageArea: self.vbox.remove(self.errorMessageArea) self.errorMessageArea = None #_____________________________________________________________________ def OnMessageAreaReponse(self, widget, response_id, message_area): if response_id == gtk.RESPONSE_CLOSE: self.OnMessageAreaClose(widget, message_area) #_____________________________________________________________________ def OnScroll(self, widget): """ Callback for "value-changed" signal from scrollbar. Updates the Project playhead position. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. """ pos = widget.get_value() self.project.SetViewStart(pos) #_____________________________________________________________________ def OnZoom(self, widget): """ Updates the viewing scale for the Project when the user zooms in or out. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. """ # The left and right sides of the viewable area rightPos = self.project.viewStart + self.scrollRange.page_size leftPos = self.project.viewStart currentPos = self.project.transport.GetPosition() # Check if the playhead is currently viewable (don't force it in view if it isn't already in view) if leftPos < currentPos < rightPos: self.centreViewOnPosition = True #now do the zoom self.project.SetViewScale(widget.get_value()) #_____________________________________________________________________ def OnZoomOut(self, widget): """ Calls OnZoom when the user zooms out. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. """ tmp = self.project.viewScale * 4. / 5. #setting the value will trigger the "value-changed" signal and call OnZoom for us. self.zoomSlider.set_value(tmp) #_____________________________________________________________________ def OnZoom100(self, widget): """ This method is not currently used (it was used when the zoom buttons existed). It's left here for future use. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. """ self.project.SetViewScale(25.0) #_____________________________________________________________________ def OnZoomIn(self, widget): """ Calls OnZoom when the user zooms in. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. """ tmp = self.project.viewScale * 1.25 #setting the value will trigger the "value-changed" signal and call OnZoom for us. self.zoomSlider.set_value(tmp) #_____________________________________________________________________ def OnZoomReset(self, widget, mouse): """ Calls OnZoom when the user resets the zoom to the default by right-clicking. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. mouse -- reserved for GTK callbacks, don't use it explicitly. """ if mouse.button == 3: tmp = (self.ZOOM_MAX_SCALE - self.ZOOM_MIN_SCALE) / 2 #setting the value will trigger the "value-changed" signal and call OnZoom for us. self.zoomSlider.set_value(tmp) return True #_____________________________________________________________________ def OnViewStartChanged(self, project): """ Callback for when the project notifies that the viewable start position has changed. Parameters: project -- The project instance that send the signal. """ self.scrollRange.value = project.viewStart #_____________________________________________________________________ def OnMouseDown(self, widget, mouse): """ Callback for "button_press_event" (not catered for, by any button presses or other mouse handlers). Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. mouse -- reserved for GTK callbacks, don't use it explicitly. """ # If we're here then we're out of bounds of anything else # So we should clear any selected events if mouse.type == gtk.gdk._2BUTTON_PRESS or mouse.type == gtk.gdk._3BUTTON_PRESS: return True self.project.ClearEventSelections() self.project.SelectInstrument(None) #_____________________________________________________________________ def OnEmptySpaceDoubleClicked(self, widget, mouse): """ Callback for "button_press_event" (not catered for, by any button presses or other mouse handlers). Shows the add instrument dialog when the empty space is double clicked. Parameters: widget -- reserved for GTK callbacks, don't use it explicitly. mouse -- reserved for GTK callbacks, don't use it explicitly. """ if mouse.type == gtk.gdk._2BUTTON_PRESS: self.mainview.OnShowAddInstrumentDialog() #_____________________________________________________________________ def OnDragDataReceived(self, widget, context, x, y, selection, targetType, time): """ Called when the user releases MOUSE1, finishing a drag and drop procedure. Adds an instrument and event for each "file://"-uri in the uri-list to the Instrument, one after the other. The files will be copied to the Project's audio directory. Parameters: widget -- InstrumentViewer being dragged. context -- reserved for GTK callbacks, don't use it explicitly. x -- point in the X axis the dragged object was dropped. y -- point in the Y axis the dragged object was dropped.. selection -- selected object area that was dragged. targetType -- mimetype of the dragged object. time -- reserved for GTK callbacks, don't use it explicitly. Returns: True -- continue GTK signal propagation. *CHECK* """ # Splitlines to separate the uri's, unquote to decode the uri-encoding ('%20' -> ' ') uris = [urllib.unquote(uri) for uri in selection.data.splitlines()] self.project.AddInstrumentAndEvents(uris, True) # True: copy context.finish(True, False, time) return True #_____________________________________________________________________ def OnDragMotion(self, widget, context, x, y, time): """ Called each time the user moves the mouse onto this widget while dragging. Parameters: widget -- InstrumentViewer the mouse is hovering over. context -- cairo widget context. x -- reserved for GTK callbacks, don't use it explicitly. y -- reserved for GTK callbacks, don't use it explicitly. time -- reserved for GTK callbacks, don't use it explicitly. Returns: True -- continue GTK signal propagation. *CHECK* """ context.drag_status(gtk.gdk.ACTION_COPY, time) return True #_____________________________________________________________________ def ChangeSize(self, small): """ Alters the size of the instrument lanes and removes the zoom buttons. Parameters: small -- True if changing to small. Otherwise False. """ #if the requested size has not changed then quit if small == self.small: return self.small = small children = self.instrumentBox.get_children() for instrView in children: instrView.ChangeSize(small) if self.small: self.inbutton.hide() self.outbutton.hide() self.zoomSlider.hide() else: self.inbutton.show() self.outbutton.show() self.zoomSlider.show() #____________________________________________________________________ def OnRestoreMessageAreaClose(self, widget=None, msg_area=None): if self.restoreMessageArea: self.vbox.remove(self.restoreMessageArea) self.restoreMessageArea = None #____________________________________________________________________ def OnRestoreMessageAreaResponse(self, widget, response_id, msg_area): if response_id == gtk.RESPONSE_OK: self.project.DoIncrementalRestore() self.OnRestoreMessageAreaClose() #____________________________________________________________________ def OnProjectIncSave(self, project): self.OnRestoreMessageAreaClose() #____________________________________________________________________ #=========================================================================