#
# THIS FILE IS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL. SEE
# THE 'COPYING' FILE FOR DETAILS
#
# EventViewer.py
#
# This module is the gui class that represents an event.
# It handles the drawing of the waveform, audio fades, and
# anything else that happens when you click a rectangular
# event on the gui.
#
#-------------------------------------------------------------------------------
import gtk
import cairo
from Project import Project
import Utils
import os, sys
import gettext
_ = gettext.gettext
import Globals
import itertools
#=========================================================================
class EventViewer(gtk.DrawingArea):
"""
The EventViewer class handles displaying a single event as part
of an EventLaneViewer object.
"""
""" GTK widget name """
__gtype_name__ = 'EventViewer'
#the width of the stroke above the fill (the line on the top of the waveform)
_LINE_WIDTH = 2
#the minimum distance allowed between each sample point in the waveform
#making this bigger will make the waveform less crowed but also less detailed
_MIN_POINT_SEPARATION = 2
#the width and height of the volume curve handles
_PIXX_FADEMARKER_WIDTH = 30
_PIXY_FADEMARKER_HEIGHT = 11
"""
Various color configurations:
ORGBA = Offset, Red, Green, Blue, Alpha
RGBA = Red, Green, Blue, Alpha
RGB = Red, Green, Blue
"""
_OPAQUE_GRADIENT_STOP_ORGBA = (0.2, 114./255, 159./255, 207./255, 1)
_TRANSPARENT_GRADIENT_STOP_ORGBA = (1, 52./255, 101./255, 164./255, 1)
_BORDER_RGB = (32./255, 74./255, 135./255)
_BORDER_HIGHLIGHT_RGB = (0, 0, 1)
_BACKGROUND_RGB = (1, 1, 1)
_TEXT_RGB = (0, 0, 0)
_SELECTED_RGBA = (0, 0, 1, 0.2)
_SELECTION_RGBA = (0, 0, 1, 0.5)
_FADEMARKERS_RGBA = (1, 0, 0, 0.8)
_PLAY_POSITION_RGB = (1, 0, 0)
_HIGHLIGHT_POSITION_RGB = (0, 0, 1)
_FADELINE_RGB = (1, 0.6, 0.6)
#_____________________________________________________________________
def __init__(self, lane, project, event, height, mainview, small=False):
"""
Creates a new instance of EventViewer.
Parameters:
lane -- parent EventLaneViewer for this instance.
project -- the currently active Project.
event -- Event drawn by this EventViewer.
height -- height in pixels for this EventViewer.
mainview -- the parent MainApp Jokosher window.
small - set to True if we want small edit views (i.e. for mixing view).
"""
self.small = small
gtk.DrawingArea.__init__(self)
self.set_events(gtk.gdk.POINTER_MOTION_MASK |
gtk.gdk.BUTTON_RELEASE_MASK |
gtk.gdk.BUTTON_PRESS_MASK |
gtk.gdk.LEAVE_NOTIFY_MASK |
gtk.gdk.KEY_PRESS_MASK)
self.connect("expose_event",self.OnDraw)
self.connect("motion_notify_event", self.OnMouseMove)
self.connect("leave_notify_event", self.OnMouseLeave)
self.connect("button_press_event", self.OnMouseDown)
self.connect("focus-in-event", self.OnFocus)
self.connect("focus-out-event", self.OnFocusLost)
self.connect("key_press_event", self.OnKeyPress)
self.connect("key_release_event", self.OnKeyRelease)
self.connect("button_release_event", self.OnMouseUp)
self.height = height # Height of this object in pixels
self.event = event # The event this widget is representing
self.project = project # A reference to the open project
self.isDragging = False # True if this event is currently being dragged
# Selections--marking part of the waveform. Don't confuse this with
# self.event.isSelected, which means the whole waveform is selected.
self.isSelecting = False # True if a selection is currently being set
self.isDraggingFade = False # True if the user is dragging a fade marker
self.lane = lane # The parent lane for this object
self.currentScale = 0 # Tracks if the project viewScale has changed
self.redrawWaveform = False # Force redraw the cached waveform on next expose event
#boolean; if the drawer should be at the left of current selection
#otherwise it will be put on the right
self.drawerAlignToLeft = True
self.fadeMarkers = [100,100] #the values of the right and left fade markers on the selection
# Set accessibility helpers
self.SetAccessibleName()
self.set_property("can-focus", True)
# sourceSmall/Large are offscreen canvases to hold our waveform images
self.sourceSmall = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)
self.sourceLarge = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)
# rectangle of cached draw areas
self.cachedDrawAreaSmall = gtk.gdk.Rectangle(0, 0, 0, 0)
self.cachedDrawAreaLarge = gtk.gdk.Rectangle(0, 0, 0, 0)
# Monitor the things this object cares about
self.project.connect("zoom", self.OnProjectZoom)
self.event.connect("waveform", self.OnEventWaveform)
self.event.connect("position", self.OnEventPosition)
self.event.connect("length", self.OnEventLength)
self.event.connect("corrupt", self.OnEventCorrupt)
self.event.connect("loading", self.OnEventLoading)
self.event.connect("selected", self.OnEventSelected)
# This defines where the blue cursor indicator should be drawn (in pixels)
self.highlightCursor = None
self.fadeMarkersContext = None
self.splitImg = gtk.gdk.pixbuf_new_from_file(os.path.join(Globals.IMAGE_PATH, "icon_split.png"))
self.cancelImg = cairo.ImageSurface.create_from_png(os.path.join(Globals.IMAGE_PATH, "icon_cancel.png"))
self.cancelButtonArea = gtk.gdk.Rectangle(85, 3, self.cancelImg.get_width(), self.cancelImg.get_height())
# drawer: this will probably be its own object in time
self.drawer = gtk.HBox()
trimButton = gtk.Button()
trimimg = gtk.Image()
trimimg.set_from_file(os.path.join(Globals.IMAGE_PATH, "icon_trim.png"))
trimButton.set_image(trimimg)
trimButton.set_tooltip_text(_("Trim"))
self.drawer.add(trimButton)
trimButton.connect("clicked", self.TrimToSelection)
delFPButton = gtk.Button()
delimg = gtk.Image()
delimg.set_from_file(os.path.join(Globals.IMAGE_PATH, "icon_fpdelete.png"))
delFPButton.set_image(delimg)
self.drawer.add(delFPButton)
delFPButton.connect("clicked", self.DeleteSelectedFadePoints)
delFPButton.set_tooltip_text(_("Delete Fade Points"))
snapFPButton = gtk.Button()
snapimg = gtk.Image()
snapimg.set_from_file(os.path.join(Globals.IMAGE_PATH, "icon_fpsnap.png"))
snapFPButton.set_image(snapimg)
self.drawer.add(snapFPButton)
snapFPButton.connect("clicked", self.SnapSelectionToFadePoints)
snapFPButton.set_tooltip_text(_("Snap To Fade Points"))
self.drawer.set_sensitive(not self.event.isLoading)
self.drawer.show()
self.mainview = mainview
self.messageID = None
self.volmessageID = None
self.selmessageID = None
#_____________________________________________________________________
def OnDraw(self, widget, event):
"""
Blits the waveform data onto the screen, and then draws the play
cursor over it.
widget -- GTK widget to be drawn.
event -- GTK event associated to the widget.
Returns:
False -- stop propagating the GTK signal. *CHECK*
"""
if self.small:
cache = self.cachedDrawAreaSmall
source = self.sourceSmall
else:
cache = self.cachedDrawAreaLarge
source = self.sourceLarge
area = event.area
#check if the expose area is within the already cached rectangle
if area.x < cache.x or (area.x + area.width > cache.x + cache.width) or self.redrawWaveform:
self.DrawWaveform(event.area)
if self.small:
cache = self.cachedDrawAreaSmall
source = self.sourceSmall
else:
cache = self.cachedDrawAreaLarge
source = self.sourceLarge
# Get a cairo surface for this drawing op
context = widget.window.cairo_create()
# Give it our waveform image as a source
context.set_source_surface(source, cache.x, cache.y)
# Blit our waveform across
context.paint()
# Overlay an extra rect if we're selected
if self.event.isSelected:
context.rectangle(event.area.x, event.area.y,
event.area.width, event.area.height)
context.set_source_rgba(*self._SELECTED_RGBA)
context.fill()
bx, by, bwidth, bheight = self.get_allocation()
context.rectangle(0, 0, bwidth, bheight)
# Draw the border
if self.is_focus():
# Highlight the border if we have focus
context.set_source_rgb(*self._BORDER_HIGHLIGHT_RGB)
else:
context.set_source_rgb(*self._BORDER_RGB)
context.stroke()
context.set_line_width(2)
#Draw play position
# TODO: don't calculate pixel position based on self.event.start, it will have rounding errros
# instead determine the pixel position of the start of our widget and subtract that from GetPixelPosition().
x = self.project.transport.GetPixelPosition(self.event.start)
context.set_line_width(1)
context.set_antialias(cairo.ANTIALIAS_NONE)
context.move_to(x+0.5, 0)
context.line_to(x+0.5, self.allocation.height)
context.set_source_rgb(*self._PLAY_POSITION_RGB)
context.stroke()
#Don't draw any cut markers, cause we cant cut while recording!
if self.event.instrument.project.GetIsRecording():
return
# Draw the highlight cursor if it's over us and we're not dragging a fadeMarker
if self.highlightCursor and not self.isDraggingFade and not self.event.isLoading:
context.move_to(self.highlightCursor, 0)
context.line_to(self.highlightCursor, self.allocation.height)
context.set_source_rgb(*self._HIGHLIGHT_POSITION_RGB)
context.set_dash([3,1],1)
context.stroke()
widget.window.draw_pixbuf(None, self.splitImg, 0, 0, int(self.highlightCursor) - int(self.splitImg.get_width() / 2) , 0)
# Overlay an extra rect if there is a selection
self.fadeMarkersContext = None
if self.event.selection != [0,0]:
x1,x2 = self.GetSelectionAsPixels()
if x2 < x1:
x2,x1 = x1,x2
context.rectangle(x1, 0, x2 - x1, event.area.height)
context.set_source_rgba(*self._SELECTION_RGBA)
context.fill()
#subtract fade marker height so that it is not drawn partially offscreen
padded_height = self.allocation.height - self._PIXY_FADEMARKER_HEIGHT
# and overlay the fademarkers
context.set_source_rgba(*self._FADEMARKERS_RGBA)
pixxFM_left = x1 + 1
#if there is enough room on the left of the selection,
#place the fademarker outside the selection bounds.
if x1 + 1 >= self._PIXX_FADEMARKER_WIDTH:
pixxFM_left -= self._PIXX_FADEMARKER_WIDTH
pixyFM_left = int(padded_height * (100-self.fadeMarkers[0]) / 100.0)
context.rectangle(pixxFM_left, pixyFM_left,
self._PIXX_FADEMARKER_WIDTH , self._PIXY_FADEMARKER_HEIGHT)
pixxFM_right = x2
#if there is enough room on the right of the selection,
#place the fademarker outside the selection bounds.
if x2 + self._PIXX_FADEMARKER_WIDTH > event.area.width:
pixxFM_right -= self._PIXX_FADEMARKER_WIDTH
pixyFM_right = int(padded_height * (100-self.fadeMarkers[1]) / 100.0)
context.rectangle(pixxFM_right, pixyFM_right,
self._PIXX_FADEMARKER_WIDTH, self._PIXY_FADEMARKER_HEIGHT)
context.fill()
context.set_source_rgba(1,1,1,1)
context.move_to(pixxFM_left + 1, pixyFM_left + self._PIXY_FADEMARKER_HEIGHT - 1)
context.show_text("%s%%" % int(self.fadeMarkers[0]))
context.move_to(pixxFM_right + 1, pixyFM_right + self._PIXY_FADEMARKER_HEIGHT - 1)
context.show_text("%s%%"% int(self.fadeMarkers[1]))
context.stroke()
# redo the rectangles so they're the path and we can in_fill() check later
context.rectangle(pixxFM_left, pixyFM_left,
self._PIXX_FADEMARKER_WIDTH, self._PIXY_FADEMARKER_HEIGHT)
context.rectangle(pixxFM_right, pixyFM_right,
self._PIXX_FADEMARKER_WIDTH, self._PIXY_FADEMARKER_HEIGHT)
self.fadeMarkersContext = context
return False
#_____________________________________________________________________
def DrawWaveform(self, exposeArea):
"""
Uses Cairo to draw the waveform level information onto a canvas in memory.
Parameters:
exposeArea -- Cairo exposed area in which to draw the waveform.
"""
allocArea = self.get_allocation()
rect = gtk.gdk.Rectangle(exposeArea.x - exposeArea.width, exposeArea.y,
exposeArea.width*3, exposeArea.height)
#Check if our area to cache is outside the allocated area
if rect.x < 0:
rect.x = 0
if rect.x + rect.width > allocArea.width:
rect.width = allocArea.width - rect.x
#set area to record where the cached surface goes
if self.small:
self.cachedDrawAreaSmall = rect
self.sourceSmall = cairo.ImageSurface(cairo.FORMAT_ARGB32,
rect.width, rect.height)
context = cairo.Context(self.sourceSmall)
else:
self.cachedDrawAreaLarge = rect
self.sourceLarge = cairo.ImageSurface(cairo.FORMAT_ARGB32,
rect.width, rect.height)
context = cairo.Context(self.sourceLarge)
context.set_line_width(2)
context.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
# Draw white background
context.rectangle(0, 0, rect.width, rect.height)
context.set_source_rgb(*self._BACKGROUND_RGB)
context.fill()
if self.event.levels_list and (self.event.duration or self.event.loadingLength):
if self.event.loadingLength:
duration = self.event.loadingLength
else:
duration = self.event.duration
context.move_to(0,rect.height)
levels = self.event.GetFadeLevels()
length = len(levels)
# time offset of the start of the drawing area in milliseconds
starting_time = int(rect.x / self.project.viewScale * 1000)
starting_index = levels.find_endtime_index(starting_time)
x = 0
last_x = -2
skip_list = []
iterator = itertools.islice(levels, starting_index, length)
for endtime, peak in iterator:
x = int((endtime - starting_time) * self.project.viewScale / 1000)
peakOnScreen = int(peak * rect.height / sys.maxint)
skip_list.append(peakOnScreen)
if (x - last_x) < self._MIN_POINT_SEPARATION:
continue
peakOnScreen = sum(skip_list) / len(skip_list)
context.line_to(x, rect.height - peakOnScreen)
skip_list = []
last_x = x
if x > rect.width:
break
context.line_to(x, rect.height)
#levels gradient fill
gradient = cairo.LinearGradient(0.0, 0.0, 0, rect.height)
gradient.add_color_stop_rgba(*self._OPAQUE_GRADIENT_STOP_ORGBA)
gradient.add_color_stop_rgba(*self._TRANSPARENT_GRADIENT_STOP_ORGBA)
context.set_source(gradient)
context.fill_preserve()
#levels path (on top of the fill)
context.set_source_rgb(*self._BORDER_RGB)
context.set_line_join(cairo.LINE_JOIN_ROUND)
context.set_line_width(self._LINE_WIDTH)
context.stroke()
if self.event.audioFadePoints:
pixelPoints = []
# draw the fade line
context.set_source_rgb(*self._FADELINE_RGB)
firstPoint = self.event.audioFadePoints[0]
pixx = self.PixXFromSec(firstPoint[0]) - rect.x
pixy = self.PixYFromVol(firstPoint[1])
context.move_to(pixx, pixy)
for sec, vol in self.event.audioFadePoints[1:]:
pixx = self.PixXFromSec(sec) - rect.x
pixy = self.PixYFromVol(vol)
pixelPoints.append((pixx, pixy))
context.line_to(pixx,pixy)
context.stroke()
#draw the fade points
for pixx, pixy in pixelPoints:
context.arc(pixx, pixy, 3.5, 0, 7)
context.fill()
# Reset the drawing scale
context.identity_matrix()
context.scale(1.0, 1.0)
#check if we are at the beginning
if rect.x == 0:
context.set_source_rgb(*self._TEXT_RGB)
context.move_to(5, 15)
if self.event.isLoading:
# Write "Loading..." or "Downloading..."
if self.event.duration <= 0:
# for some file types gstreamer doesn't give us a duration
# so don't display the percentage
if self.event.isDownloading:
message = _("Downloading...")
else:
message = _("Loading...")
else:
displayLength = int(100 * self.event.loadingLength / self.event.duration)
if self.event.isDownloading:
message = _("Downloading (%d%%)...") % displayLength
else:
message = _("Loading (%d%%)...") % displayLength
# show the appropriate message
context.show_text(message)
# display a cancel button
self.cancelButtonArea.x = context.get_current_point()[0]+3 # take the current context.x and pad it a bit
context.set_source_surface(self.cancelImg, self.cancelButtonArea.x, self.cancelButtonArea.y)
context.paint()
elif self.event.isRecording:
context.show_text(_("Recording..."))
else:
#Draw event name
context.show_text(self.event.name)
self.redrawWaveform = False
#_____________________________________________________________________
def Destroy(self):
"""
Called when the EventViewer gets destroyed.
It also destroys any child widget and disconnects itself from any
gobject signals.
"""
self.project.disconnect_by_func(self.OnProjectZoom)
self.event.disconnect_by_func(self.OnEventSelected)
self.event.disconnect_by_func(self.OnEventCorrupt)
self.event.disconnect_by_func(self.OnEventLength)
self.event.disconnect_by_func(self.OnEventLoading)
self.event.disconnect_by_func(self.OnEventPosition)
self.event.disconnect_by_func(self.OnEventWaveform)
#delete the cached images
del self.sourceSmall
del self.sourceLarge
del self.cancelImg
self.destroy()
#_____________________________________________________________________
def OnMouseMove(self, widget, mouse):
"""
Display a message in the StatusBar when the mouse hovers over the
EventViewer.
Also displays cursors depending on the current action being performed.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- GTK mouse event that fired this method call.
Returns:
True -- stop GTK signal propagation.
"""
if not self.window or self.event.instrument.project.GetIsRecording():
return True #don't let the intrument viewer handle it
# display status bar message if has not already been displayed
if not self.messageID:
self.messageID = self.mainview.SetStatusBar(_("To Split, Double-Click the wave - To Select, Shift-Click and drag the mouse"))
if self.isDraggingFade:
#subtract half the fademarker height so it doesnt go half off the screen
cur_pos = (mouse.y - (self._PIXY_FADEMARKER_HEIGHT / 2))
height = self.allocation.height - self._PIXY_FADEMARKER_HEIGHT
percent = cur_pos / float(height)
#set percent between 0 and 1
percent = min(1, max(0, percent))
self.fadeMarkers[self.fadeBeingDragged] = 100 - int(percent * 100)
self.queue_draw()
if not self.volmessageID:
self.volmessageID = self.mainview.SetStatusBar(_("Drag the red sliders to modify the volume fade."))
return True
if self.fadeMarkersContext and self.fadeMarkersContext.in_fill(mouse.x, mouse.y):
# quit this function now, so the highlightCursor doesn't move
# while you're over a fadeMarker
return True
if self.isDragging:
ptr = gtk.gdk.display_get_default().get_pointer()
x = ptr[1]
y = ptr[2]
dx = float(x - self.mouseAnchor[0]) / self.project.viewScale
time = self.event.start + dx
time = max(0, time)
if self.event.MayPlace(time):
self.event.start = time
self.lane.UpdatePosition(self)
self.mouseAnchor = [x, y]
else:
temp = self.event.start
self.event.MoveButDoNotOverlap(time)
self.lane.UpdatePosition(self)
#MoveButDoNotOverlap() moves the event out of sync with the mouse
#and the mouseAnchor must be updated manually.
delta = (self.event.start - temp) * self.project.viewScale
self.mouseAnchor[0] += int(delta)
self.highlightCursor = None
elif self.isSelecting:
x2 = max(0,min(self.allocation.width,mouse.x))
self.event.selection[1] = self.SecFromPixX(x2)
self.UpdateFadeMarkers()
selection_direction = "ltor"
selection = self.event.selection
if selection[0] > selection[1]:
selection_direction = "rtol"
self.fadeMarkers.reverse()
#set the drawer align position
self.drawerAlignToLeft = (selection_direction == "rtol")
#move the drawer to its proper position
self.UpdateDrawerPosition(selection_direction == "rtol")
else:
self.highlightCursor = mouse.x
self.queue_draw()
return True
#_____________________________________________________________________
def OnMouseDown(self, widget, mouse):
"""
Called when the user pressed a mouse button.
Possible click combinations to capture:
{L|R}MB: deselect all Events, remove any existing selection in
this Event then select this Event and begin moving the Event.
LMB+shift: remove any existing selection in this Event and begin
selecting part of this Event.
{L|R}MB+ctrl: select this Event without deselecting other Events.
RMB: display a context menu.
LMB double-click: split this Event here.
LMB over a fadeMarker: drag the correspondent marker.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- GTK mouse event that fired this method call.
Returns:
True -- continue GTK signal propagation. *CHECK*
"""
#Don't allow moving, etc while recording!
if self.event.instrument.project.GetIsRecording():
return True #don't let the instrument viewer handle this click
self.grab_focus()
# {L|R}MB: deselect all events, select this event, begin moving the event
# {L|R}MB+ctrl: select this event without deselecting other events
if 'GDK_CONTROL_MASK' not in mouse.state.value_names:
self.project.ClearEventSelections()
self.project.SelectInstrument(None)
self.event.SetSelected(True)
#Don't allow editing while playing back.
#It must be here to avoid afecting the selection behavior
if self.mainview.isPlaying or self.mainview.isPaused:
return True
# RMB: context menu
if mouse.button == 3:
self.ContextMenu(mouse)
elif mouse.button == 1:
# check to see if the user clicked on the cancel button
if self.cancelButtonArea.x <= mouse.x <= self.cancelButtonArea.width+self.cancelButtonArea.x \
and self.cancelButtonArea.y <= mouse.y <= self.cancelButtonArea.height+self.cancelButtonArea.y \
and self.event.isLoading:
self.OnDelete()
return True
if 'GDK_SHIFT_MASK' in mouse.state.value_names:
# LMB+shift: remove any existing selection in this event, begin
# selecting part of this event
self.isSelecting = True
self.event.selection[0] = self.SecFromPixX(mouse.x)
self.fadeMarkers = [100,100]
if not self.selmessageID:
self.selmessageID = self.mainview.SetStatusBar(_("Click the buttons below the selection to do something to that portion of audio."))
else:
if self.fadeMarkersContext and self.fadeMarkersContext.in_fill(mouse.x, mouse.y):
# LMB over a fadeMarker: drag that marker
self.isDraggingFade = True
if mouse.x > self.PixXFromSec(self.event.selection[1]) - self._PIXX_FADEMARKER_WIDTH - 1:
self.fadeBeingDragged = 1
return True
else:
self.fadeBeingDragged = 0
return True
if mouse.type == gtk.gdk._2BUTTON_PRESS:
# LMB double-click: split here
self.mouseAnchor[0] = mouse.x
if self.event.isLoading == False:
self.OnSplit(None, mouse.x)
return True
# remove any existing selection in this event
self.event.selection = [0,0]
if self.drawer.parent == self.lane.fixed:
self.lane.fixed.remove(self.drawer)
if self.volmessageID: #clesr status bar if not already clear
self.mainview.ClearStatusBar(self.volmessageID)
self.volmessageID = None
if self.selmessageID: #clesr status bar if not already clear
self.mainview.ClearStatusBar(self.selmessageID)
self.selmessageID = None
self.isDragging = True
self.eventStart = self.event.start
ptr = gtk.gdk.display_get_default().get_pointer()
self.mouseAnchor = [ptr[1], ptr[2]]
return True
#_____________________________________________________________________
def OnFocus(self, widget, event):
"""
Select the event when focused via a non-mouse based method.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
event -- GTK focus event that fired this method call.
Returns:
True -- continue GTK signal propagation after processing event.
False -- pass this event on to other handlers because we don't want it.
"""
self.queue_draw()
return True
#_____________________________________________________________________
def OnFocusLost(self, widget, event):
"""
Deselect the event when focus is lost.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
event -- GTK focus event that fired this method call.
Returns:
True -- continue GTK signal propagation after processing the event.
"""
self.queue_draw()
return True
#_____________________________________________________________________
def OnKeyPress(self, widget, event):
"""
Handle manipulation of events via the keyboard.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
event -- GTK keyboard event that fired this method call.
Returns:
True -- continue GTK signal propagation after processing the event.
False -- pass this event on to other handlers because we don't want it.
"""
#Don't allow moving, etc while recording!
if self.event.instrument.project.GetIsRecording():
return False
modifier = 0.1 # Multiply movement by this amount (modified by ctrl key)
moveCursor = False # Are we moving the highlight cursor or the event?
moveLeftFade = False
moveRightFade = False
moveTo = None
if "GDK_SHIFT_MASK" in event.state.value_names:
if self.event.selection != [0, 0]:
moveLeftFade = True
moveCursor = True
modifier = 0.5
if "GDK_CONTROL_MASK" in event.state.value_names:
if self.event.selection != [0, 0]:
moveRightFade = True
modifier *= 10
if "GDK_MOD1_MASK" in event.state.value_names:
moveCursor = True
modifier *= 10
if not self.isSelecting:
if not self.highlightCursor:
self.event.selection[0] = 0
else:
self.event.selection[0] = self.SecFromPixX(self.highlightCursor)
self.UpdateFadeMarkers()
self.isSelecting = True
selection_direction = "ltor"
selection = self.event.selection
if selection[0] > selection[1]:
selection_direction = "rtol"
self.fadeMarkers.reverse()
key = gtk.gdk.keyval_name(event.keyval)
if key == "Return":
# Toggle if this event is selected or not
self.event.SetSelected(not self.event.isSelected)
# Clear any selection that has been made
self.event.selection = [0, 0]
self.isSelecting = False
self.highlightCursor = None
self.HideDrawer()
return True
if not self.event.isSelected:
# If this event isn't selected don't process any key events for it (except
return False
if key == "Up":
# Adjust fade points
if moveLeftFade:
self.fadeMarkers[0] += 1
if moveRightFade:
self.fadeMarkers[1] += 1
if self.fadeMarkers[0] > 100:
self.fadeMarkers[0] = 100
if self.fadeMarkers[1] > 100:
self.fadeMarkers[1] = 100
if moveLeftFade or moveRightFade:
self.SetAudioFadePointsFromCurrentSelection()
elif key == "Down":
# Adjust fade points
if moveLeftFade:
self.fadeMarkers[0] -= 1
if moveRightFade:
self.fadeMarkers[1] -= 1
if self.fadeMarkers[0] < 0:
self.fadeMarkers[0] = 0
if self.fadeMarkers[1] < 0:
self.fadeMarkers[1] = 0
if moveLeftFade or moveRightFade:
self.SetAudioFadePointsFromCurrentSelection()
elif key == "Left":
# Move event/highlight cursor
if not self.isSelecting:
# Reset selection
self.event.selection = [0, 0]
self.fadeMarkers = [100, 100]
if not self.highlightCursor:
self.event.select = [0, 0]
self.fadeMarkers = [100, 100]
self.highlightCursor = 0
if moveCursor:
moveTo = self.highlightCursor - modifier
else:
moveTo = self.event.start - modifier
elif key == "Right":
# Move event/highlight cursor
if not self.isSelecting:
# Reset selection
self.event.selection = [0, 0]
self.fadeMarkers = [100, 100]
if not self.highlightCursor:
self.highlightCursor = 0
if moveCursor:
moveTo = self.highlightCursor + modifier
else:
moveTo = self.event.start + modifier
elif key == "space" and not self.event.isLoading:
if self.highlightCursor:
# If we've got the highlight cursor out cut at that point
self.OnSplit(None, self.highlightCursor)
else:
# Otherwise, stop playing and cut at the play position (if it's over this event)
play_pos = self.project.transport.GetPixelPosition(self.event.start)
if play_pos > 0 and play_pos < self.allocation.width:
self.project.Stop()
self.OnSplit(None, play_pos)
return True
else:
return False
if moveTo:
# Don't go beyond respective boundaries
if moveTo < 0:
moveTo = 0
if (moveCursor or self.isSelecting) and moveTo > self.allocation.width:
moveTo = self.allocation.width
if moveCursor:
self.highlightCursor = moveTo
else:
self.event.MoveButDoNotOverlap(moveTo)
if self.isSelecting:
self.event.selection[1] = self.SecFromPixX(moveTo)
# Hide the drawer if the selection has been cleared
if self.event.selection == [0, 0]:
self.HideDrawer()
self.lane.UpdatePosition(self)
return True
#_____________________________________________________________________
def OnKeyRelease(self, widget, event):
"""
Handle releasing of ALT key to stop drawing selection.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
event -- GTK keyboard event that fired this method call.
Returns:
True -- continue GTK signal propagation after processing the event.
False -- pass this event on to other handlers because we don't want it.
"""
key = gtk.gdk.keyval_name(event.keyval)
if self.isSelecting and "GDK_MOD1_MASK" in event.state.value_names and (key == "Left" or key == "Right"):
self.isSelecting = False
self.highlightCursor = None
self.ShowDrawer()
# Allow this even to be processed by other widgets regardless (so accelerators still work)
return False
#_____________________________________________________________________
def ContextMenu(self, mouse):
"""
Creates a context menu in response to a right click.
Parameters:
mouse -- GTK mouse event that fired this method call.
"""
menu = gtk.Menu()
splitImg = gtk.Image()
splitImg.set_from_file(os.path.join(Globals.IMAGE_PATH, "icon_split.png"))
items = [ (_("_Split"), self.OnSplit, True, splitImg, mouse.x),
("---", None, None, None, None),
(_("Cu_t"), self.OnCut, True, gtk.image_new_from_stock(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU), None),
(_("_Copy"), self.OnCopy, True, gtk.image_new_from_stock(gtk.STOCK_COPY, gtk.ICON_SIZE_MENU), None),
(_("_Delete"), self.OnDelete, False, gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU), None)
]
for label, callback, sometimes, image, param in items:
if label == "---":
menuItem = gtk.SeparatorMenuItem()
elif image:
menuItem = gtk.ImageMenuItem(label, True)
menuItem.set_image(image)
else:
menuItem = gtk.MenuItem(label=label)
if self.event.isLoading and sometimes:
menuItem.set_sensitive(False)
else:
menuItem.set_sensitive(True)
menuItem.show()
menu.append(menuItem)
if callback:
if param:
menuItem.connect("activate", callback, param)
else:
menuItem.connect("activate", callback)
self.highlightCursor = mouse.x
self.popupIsActive = True
menu.popup(None, None, None, mouse.button, mouse.time)
menu.connect("selection-done",self.OnMenuDone)
self.mouseAnchor = [mouse.x, mouse.y]
#_____________________________________________________________________
def OnMenuDone(self, widget):
"""
Hides the right-click context menu after the user has selected one
of its options or clicked elsewhere.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
"""
self.popupIsActive = False
self.highlightCursor = None
#_____________________________________________________________________
def OnMouseUp(self, widget, mouse):
"""
Called when the left mouse button is released.
Finishes drag, fade and selection operations.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- GTK mouse event that fired this method call.
"""
if mouse.button == 1:
if self.isDragging:
self.isDragging = False
if (self.eventStart != self.event.start):
self.event.Move(self.event.start, self.eventStart)
return False #need to pass this button release up to RecordingView
elif self.isDraggingFade:
self.isDraggingFade = False
# set the audioFadePoints appropriately
self.SetAudioFadePointsFromCurrentSelection()
elif self.isSelecting:
self.isSelecting = False
self.ShowDrawer()
#_____________________________________________________________________
def ShowDrawer(self):
"""
Cause the draw to be shown at the current event selection position.
"""
selection_direction = "ltor"
selection = self.event.selection
if selection[0] > selection[1]:
self.event.selection = [selection[1], selection[0]]
selection_direction = "rtol"
#set the drawer align position
self.drawerAlignToLeft = (selection_direction == "rtol")
#move the drawer to its proper position
self.UpdateDrawerPosition()
#_____________________________________________________________________
def OnMouseLeave(self, widget, event):
"""
Clears the StatusBar message when the mouse moves out of the
EventLaneViewer area. It also disables cursors accordingly.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- GTK mouse event that fired this method call.
"""
if self.messageID: #clear status bar if not already clear
self.mainview.ClearStatusBar(self.messageID)
self.messageID = None
self.highlightCursor = None
self.queue_draw()
#_____________________________________________________________________
def OnSplit(self, gtkevent, pos):
"""
Splits an Event in two.
Parameters:
gtkevent -- reserved for GTK callbacks, don't use it explicitly.
position -- The position in the event to split
"""
if self.event.selection != [0,0]:
undoAction = self.project.NewAtomicUndoAction()
self.event.SplitEvent(self.event.selection[1], _undoAction_=undoAction)
self.event.SplitEvent(self.event.selection[0], _undoAction_=undoAction)
self.event.selection = [0,0]
self.HideDrawer()
else:
if pos == 0.0:
return
else:
pos /= float(self.project.viewScale)
self.event.SplitEvent(pos)
#_____________________________________________________________________
def OnCut(self, gtkevent):
"""
Cuts the selected portion of the Event, and puts it on the clipboard.
Parameters:
gtkevent -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.event.selection != [0,0]:
undoAction = self.project.NewAtomicUndoAction()
self.event.SplitEvent(self.event.selection[1], _undoAction_=undoAction)
e = self.event.SplitEvent(self.event.selection[0], _undoAction_=undoAction)
self.project.clipboardList = [e]
e.Delete(_undoAction_=undoAction)
self.event.selection = [0,0]
self.HideDrawer()
else:
self.project.clipboardList = [self.event]
self.OnDelete()
#_____________________________________________________________________
def OnCopy(self, gtkevent):
"""
Copies the selected portion of the Event to the clipboard.
Parameters:
gtkevent -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.event.selection != [0,0]:
e = self.event.CopySelection()
self.project.clipboardList = [e]
#We shouldn't hide the drawer here, unfriendly behaviour
else:
self.project.clipboardList = [self.event]
#_____________________________________________________________________
def OnDelete(self, event=None):
"""
Called when "Delete" is selected from context menu.
Deletes the selected Event from the Project.
Parameters:
event -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.event.selection != [0,0]:
undoAction = self.project.NewAtomicUndoAction()
self.event.SplitEvent(self.event.selection[1], _undoAction_=undoAction)
e = self.event.SplitEvent(self.event.selection[0], _undoAction_=undoAction)
e.Delete(_undoAction_=undoAction)
self.event.selection = [0,0]
self.HideDrawer()
else:
self.event.Delete()
#_____________________________________________________________________
def TrimToSelection(self, gtkevent):
"""
Cut this Event down so only the selected bit remains. This Event
is L-S-R, where S is the selected bit; L and R will be removed.
Parameters:
gtkevent -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.event.isLoading == True:
return
self.HideDrawer()
self.event.Trim(self.event.selection[0], self.event.selection[1])
self.event.selection = [0,0]
#_____________________________________________________________________
def HideDrawer(self):
"""
Hide the drawer.
"""
self.lane.RemoveDrawer(self.drawer)
#_____________________________________________________________________
def do_size_request(self, requisition):
"""
This function has been overrided to avoid getting a 1x1 display size.
Parameters:
requisition -- TODO
"""
if self.event.duration > 0:
requisition.width = self.event.duration * self.project.viewScale
elif self.event.loadingLength > 0:
requisition.width = self.event.loadingLength * self.project.viewScale
else:
requisition.width = 1 * self.project.viewScale
if not (self.small):
requisition.height = 77
else:
requisition.height = 30
# rect = self.get_allocation()
#
# if rect.height < 30:
# requisition.height = 30
# else:
# requisition.height = rect.height
#_____________________________________________________________________
def OnEventPosition(self, event):
"""
Callback function for when the position of the event changes.
"""
self.SetAccessibleName()
self.lane.UpdatePosition(self)
#_____________________________________________________________________
def OnEventWaveform(self, event):
"""
Callback function for when the waveform of the event changes.
"""
self.redrawWaveform = True
self.UpdateFadeMarkers()
self.queue_draw()
#_____________________________________________________________________
def OnEventLoading(self, event):
"""
Callback function for when the loading status of the event changes.
"""
self.drawer.set_sensitive(not self.event.isLoading)
self.queue_draw()
#_____________________________________________________________________
def OnEventLength(self, event):
"""
Callback function for when the length of the event changes.
"""
self.redrawWaveform = True
self.SetAccessibleName()
self.queue_resize()
self.queue_draw()
#_____________________________________________________________________
def OnEventSelected(self, event):
"""
Callback function for when the event is selected or de-selected.
"""
self.queue_draw()
#_____________________________________________________________________
def OnEventCorrupt(self, event, error):
"""
Callback function for when the event's file is found to be corrupt.
"""
message="%s %s\n\n%s" % (_("Error loading file:"), self.event.filelabel,
_("Please make sure the file exists, and the appropriate plugin is installed."))
outputtext = "\n\n".join((message, error))
dlg = gtk.MessageDialog(None,
gtk.DIALOG_MODAL,
gtk.MESSAGE_ERROR,
gtk.BUTTONS_CLOSE,
outputtext)
dlg.connect('response', lambda dlg, response: dlg.destroy())
dlg.show()
self.OnDelete()
#_____________________________________________________________________
def OnProjectZoom(self, project):
"""
Callback for when the zoom level of the project changes.
Parameters:
project -- The project instance that send the signal.
"""
if self.currentScale != self.project.viewScale:
self.redrawWaveform = True
self.queue_resize()
self.currentScale = self.project.viewScale
self.queue_draw()
#_____________________________________________________________________
def PixXFromSec(self, sec):
"""
Converts seconds to an X pixel position in the waveform.
Parameters:
sec -- value in seconds.
Returns:
the correspondent pixel X position in the waveform.
"""
return round(float(sec) * self.project.viewScale)
#_____________________________________________________________________
def SecFromPixX(self, pixx):
"""
Converts an X pixel position in the waveform into seconds.
Parameters:
pixx -- X pixel position value.
Returns:
the correspondent value in seconds.
"""
return float(pixx) / self.project.viewScale
#_____________________________________________________________________
def PixYFromVol(self, vol):
"""
Converts a volume value into a Y pixel position in the waveform.
Parameters:
vol -- volume value in a [0.0, 1.0] range.
Returns:
the correspondent pixel Y position in the waveform.
"""
return round((1.0 - vol) * self.allocation.height)
#_____________________________________________________________________
def VolFromPixY(self, pixy):
"""
Converts a Y pixel position in the waveform into a volume value.
Parameters:
pixy -- Y pixel position value.
Returns:
the correspondent value, in seconds, in a [0.0, 1.0] range.
"""
return 1.0 - (float(pixy) / self.allocation.height)
#_____________________________________________________________________
def SetAudioFadePointsFromCurrentSelection(self):
"""
Creates fade points for the current selection.
"""
volLeft = self.fadeMarkers[0] / 100.0
volRight = self.fadeMarkers[1] / 100.0
selection = self.event.selection
self.event.AddAudioFadePoints(selection[0], selection[1], volLeft, volRight)
#_____________________________________________________________________
def GetSelectionAsPixels(self):
"""
Obtain the Event selection as a list of two points, measured in
pixels instead of seconds like Event.selection.
Returns:
list with two X points describing the selection.
"""
x1 = self.PixXFromSec(self.event.selection[0])
x2 = self.PixXFromSec(self.event.selection[1])
return [x1, x2]
#_____________________________________________________________________
def UpdateDrawerPosition(self, reverseSelectionPoints=False):
"""
Updates the drawer position to the correct position when user
moves the mouse over the EventViewer.
Parameters:
reverseSelectionPoints -- True if the selection points should
be reversed.
"""
if reverseSelectionPoints:
selection = [self.event.selection[1], self.event.selection[0]]
else:
selection = self.event.selection[:]
x0 = self.project.viewScale * self.event.selection[0]
x1 = (self.project.viewScale * self.event.selection[1]) - x0
if x0 < self.drawer.size_request()[0] and x1< self.drawer.size_request()[0]:
self.drawerAlignToLeft = True
eventx = int((self.event.start - self.project.viewStart) * self.project.viewScale)
if self.drawerAlignToLeft:
x = int(self.PixXFromSec(selection[0]))
else:
width = self.drawer.allocation.width
if width == 1:
width = 40 # fudge it because it has no width initially
x = int(self.PixXFromSec(selection[1]) - width)
self.lane.PutDrawer(self.drawer, eventx + x)
#don't update the lane because it calls us and that might cause infinite loop
#_____________________________________________________________________
def DeleteSelectedFadePoints(self, event):
"""
Deletes the selected fade points from the Event.
Parameters:
event -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.event.isLoading == True:
return
self.event.DeleteSelectedFadePoints()
#_____________________________________________________________________
def SnapSelectionToFadePoints(self, event):
"""
Snaps the selection to a set of fade points.
Parameters:
event -- reserved for GTK callbacks, don't use it explicitly.
"""
if len(self.event.audioFadePoints) < 2:
#not enough levels
return
points = [x[0] for x in self.event.audioFadePoints]
left, right = self.event.selection
leftOfLeft = max([x for x in points if x < left])
rightOfLeft = min([x for x in points if x >= left])
leftOfRight = max([x for x in points if x < right])
rightOfRight = min([x for x in points if x >= right])
if abs(leftOfLeft - left) < abs(rightOfLeft - left):
leftChooses = leftOfLeft
else:
leftChooses = rightOfLeft
if abs(leftOfRight - right) > abs(rightOfRight - right):
rightChooses = rightOfRight
else:
rightChooses = leftOfRight
if leftChooses == rightChooses:
#the both selected the same point
if abs(leftChooses - left) > abs(rightChooses - right):
#if right is closer to the point
leftChooses = leftOfLeft
else:
rightChooses = rightOfRight
self.event.selection = [leftChooses, rightChooses]
self.UpdateFadeMarkers()
self.queue_draw()
#_____________________________________________________________________
def UpdateFadeMarkers(self):
"""
Called when the a fade point's value changes, to update the graphical
marker over the waveform.
"""
self.fadeMarkers = [self.event.GetFadeLevelAtPoint(x) * 100 for x in self.event.selection]
#_____________________________________________________________________
def SetAccessibleName(self):
"""
Set an ATK name to help users with screenreaders.
"""
accessible = self.get_accessible()
accessible.set_name(_("Event, %(name)s, %(dur)0.2f seconds long, starting at %(start)0.2f seconds.") \
% {"name":self.event.name, "dur":self.event.duration, "start":self.event.start})
#_____________________________________________________________________
def ChangeSize(self, small):
"""
Changes size of event viewer.
Parameters:
small -- True if the event viewer is to change to small
"""
self.small = small
self.queue_resize()
self.queue_draw()
#____________________________________________________________________
#=========================================================================