#
# THIS FILE IS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL. SEE
# THE 'COPYING' FILE FOR DETAILS
#
# VUWidget.py
#
# This module draws the gradient volume levels and is used by
# MixerStrip.py to show the volume levels in Jokosher's mix view.
#
#-------------------------------------------------------------------------------
import pygtk
pygtk.require("2.0")
import gtk
import cairo
import gettext
_ = gettext.gettext
#=========================================================================
class VUWidget(gtk.DrawingArea):
"""
Draws the gradient volume levels and is used by MixerStrip.py
to show the volume levels in Jokosher's mix view.
"""
""" GTK widget name """
__gtype_name__ = 'VUWidget'
""" Height, in pixels, for the volume handle """
_VH_HEIGHT = 25
""" Space between the edge of the volume handle and the edge of the VU meter"""
_VH_PADDING = 20
_VH_BORDER_WIDTH = 5
""" the highest value allowed to be set for the volume (lowest is always zero)"""
_MAX_VOLUME = 2
"""the amount to move the volume up or down by when scrolling or pressing the arrow keys"""
_VOLUME_STEP_AMOUNT = 0.2
""" Both text height and width below depend on the font size.
If you change the font size, figure out the new pixel sizes and update these values."""
_FONT_SIZE = 18
_TEXT_HEIGHT = 13
_TEXT_WIDTH = 37
"""
Various color configurations:
ORGBA = Offset, Red, Green, Blue, Alpha
RGBA = Red, Green, Blue, Alpha
RGB = Red, Green, Blue
"""
_VH_ACTIVE_RGBA = (1., 1., 1., 1.)
_VH_INACTIVE_RGBA = (1., 1., 1., 0.75)
_VH_BORDER_RGBA = (0.5, 0., 0., 0.75)
_BACKGROUND_RGB = (0., 0., 0.)
_LEVEL_GRADIENT_BOTTOM_ORGBA = (1, 0, 1, 0, 1)
_LEVEL_GRADIENT_TOP_ORGBA = (0, 1, 0, 0, 1)
_TEXT_RGBA = (0., 0., 0., 1.)
"""
The events we wish to receive after we grab the mouse (and it is no longer above this widget)
If events other than mouse event are put in here, it may cause the program to crash.
"""
_POINTER_GRAB_EVENTS = (
gtk.gdk.POINTER_MOTION_MASK |
gtk.gdk.BUTTON_RELEASE_MASK |
gtk.gdk.BUTTON_PRESS_MASK |
gtk.gdk.LEAVE_NOTIFY_MASK )
#_____________________________________________________________________
def __init__(self, mixerstrip, mainview):
"""
Creates a new instance of VUWidget.
Parameters:
mixerstrip -- TODO
mainview -- the main Jokosher window (JokosherApp).
"""
gtk.DrawingArea.__init__(self)
self.mixerstrip = mixerstrip
self.mainview = mainview
self.set_events(self._POINTER_GRAB_EVENTS |
gtk.gdk.KEY_PRESS_MASK |
gtk.gdk.SCROLL_MASK )
self.set_flags(gtk.CAN_FOCUS)
self.connect("button-press-event", self.OnMouseDown)
self.connect("button-release-event", self.OnMouseUp)
self.connect("motion_notify_event", self.OnMouseMove)
self.connect("leave_notify_event", self.OnMouseLeave)
self.connect("configure_event", self.OnSizeChanged)
self.connect("expose-event", self.OnDraw)
self.connect("key_press_event", self.OnKeyPress)
self.connect("scroll-event", self.OnScroll)
self.fader_active = False
self.fader_hover = False
self.message_id = None
self.source = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.allocation.width, self.allocation.height)
#_____________________________________________________________________
def OnMouseDown(self, widget, mouse):
"""
If the fader widget is clicked, activates it.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- reserved for GTK callbacks, don't use it explicitly.
"""
#If the slider is right-clicked then set the volume to 100%
if mouse.button == 3:
self.mixerstrip.SetVolume(1.0)
self.queue_draw()
return True
if self.__YPosOverVolumeHandle(mouse.y):
response = gtk.gdk.pointer_grab(self.window, False, self._POINTER_GRAB_EVENTS, None, None, mouse.time)
self.fader_active = (response == gtk.gdk.GRAB_SUCCESS)
self.queue_draw()
self.grab_focus()
#Returning True is very important!! It tells other not to handle the mouse event for us.
#Without it weird thing will happen; pointer grabs will not work, and button-release-events will sometimes not be sent, etc.
return True
#_____________________________________________________________________
def OnMouseMove(self, widget, mouse):
"""
Displays a helper message in the StatusBar and sets the volume
according to the position of the fader widget.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- reserved for GTK callbacks, don't use it explicitly.
"""
if not self.message_id:
self.message_id = self.mainview.SetStatusBar(_("Drag the slider to alter volume levels - Right-Click to set the slider to 100%"))
oldHover = self.fader_hover
self.fader_hover = (0 < mouse.x < self.get_allocation().width) and self.__YPosOverVolumeHandle(mouse.y)
if self.fader_active:
rect = self.get_allocation()
volume = 1. - ((mouse.y - self._VH_HEIGHT/2) / (rect.height-self._VH_HEIGHT))
volume = max(volume, 0.)
volume = min(volume, 1.)
self.mixerstrip.SetVolume(volume * self._MAX_VOLUME)
if self.fader_active or oldHover != self.fader_hover:
self.queue_draw()
return True
#_____________________________________________________________________
def OnMouseUp(self, widget, mouse):
"""
Deactivates the fader widget.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.fader_active:
self.fader_active = False
gtk.gdk.pointer_ungrab(mouse.time)
self.queue_draw()
self.mixerstrip.CommitVolume()
return True
#_____________________________________________________________________
def OnMouseLeave(self, widget, mouse):
"""
Clears the StatusBar helper message.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.message_id:
self.mainview.ClearStatusBar(self.message_id)
self.message_id = None
if self.fader_hover:
self.fader_hover = False
self.queue_draw()
return True
#_____________________________________________________________________
def OnKeyPress(self, widget, event):
"""
Moves the volume up or down based on the key presses.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- reserved for GTK callbacks, don't use it explicitly.
"""
key = gtk.gdk.keyval_name(event.keyval)
if key == "Up" or key == "Down":
self.ChangeVolumeByStep(increaseVolume=(key == "Up"))
return True
return False
#_____________________________________________________________________
def OnScroll(self, widget, event):
"""
Moves the volume up or down based on the scroll motion.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
mouse -- reserved for GTK callbacks, don't use it explicitly.
"""
if self.fader_active:
return False
if event.direction == gtk.gdk.SCROLL_UP or event.direction == gtk.gdk.SCROLL_DOWN:
self.ChangeVolumeByStep(increaseVolume=(event.direction == gtk.gdk.SCROLL_UP))
#Don't make the handle glow if it has moved out from under the mouse
self.fader_hover = self.fader_hover = (0 < event.x < self.get_allocation().width) and self.__YPosOverVolumeHandle(event.y)
return True
return False
#_____________________________________________________________________
def ChangeVolumeByStep(self, increaseVolume):
"""
Moves the volume up or down based on the volume step size.
Parameters:
increaseVolume -- if True, volume will be increased, otherwise it will be decreased.
"""
if increaseVolume:
volume = self.mixerstrip.GetVolume() + self._VOLUME_STEP_AMOUNT
else:
volume = self.mixerstrip.GetVolume() - self._VOLUME_STEP_AMOUNT
volume = max(0, min(self._MAX_VOLUME, volume))
self.mixerstrip.SetVolume(volume)
self.queue_draw()
#_____________________________________________________________________
def OnSizeChanged(self, obj, evt):
"""
Toggles a redraw of the VUWidget if needed.
Parameters:
obj -- reserved for Cairo callbacks, don't use it explicitly. *CHECK*
evt --reserved for Cairo callbacks, don't use it explicitly. *CHECK*
"""
if self.allocation.width != self.source.get_width() or self.allocation.height != self.source.get_height():
self.source = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.allocation.width, self.allocation.height)
self.GenerateBackground()
#_____________________________________________________________________
def GenerateBackground(self):
"""
Renders the gradient strip for the VU meter background to speed up drawing.
"""
rect = self.get_allocation()
ctx = cairo.Context(self.source)
ctx.set_line_width(2)
ctx.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
# Create our green to red gradient
pat = cairo.LinearGradient(0.0, 0.0, 0, rect.height)
pat.add_color_stop_rgba(*self._LEVEL_GRADIENT_BOTTOM_ORGBA)
pat.add_color_stop_rgba(*self._LEVEL_GRADIENT_TOP_ORGBA)
# Fill the volume bar
ctx.rectangle(0, 0, rect.width, rect.height)
ctx.set_source(pat)
ctx.fill()
#_____________________________________________________________________
def OnDraw(self, widget, event):
"""
Handles the GTK paint event.
Parameters:
widget -- reserved for GTK callbacks, don't use it explicitly.
event -- reserved for GTK callbacks, don't use it explicitly.
Returns:
False -- TODO
"""
ctx = widget.window.cairo_create()
ctx.save()
rect = self.get_allocation()
# Fill a black background
ctx.rectangle(0, 0, rect.width, rect.height)
ctx.set_source_rgb(*self._BACKGROUND_RGB)
ctx.fill()
# Blit across the cached gradient backgound
ctx.rectangle(0, rect.height * (1. - self.mixerstrip.GetLevel()), rect.width, rect.height)
ctx.clip()
ctx.set_source_surface(self.source, 0, 0)
ctx.paint()
# Restore the clipping region from saved context
ctx.restore()
# Draw the current volume level bar, with highlight if appropriate
vpos = self.__GetVolumeHandleYPos()
if self.fader_active:
ctx.set_source_rgba(*self._VH_BORDER_RGBA)
ctx.set_line_width(self._VH_HEIGHT + self._VH_BORDER_WIDTH)
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
ctx.move_to(self._VH_PADDING, vpos)
ctx.line_to(rect.width - self._VH_PADDING, vpos)
ctx.stroke()
ctx.set_source_rgba(*self._VH_ACTIVE_RGBA)
elif self.fader_hover:
ctx.set_source_rgba(*self._VH_ACTIVE_RGBA)
else:
ctx.set_source_rgba(*self._VH_INACTIVE_RGBA)
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
ctx.set_line_width(self._VH_HEIGHT)
ctx.move_to(self._VH_PADDING, vpos)
ctx.line_to(rect.width - self._VH_PADDING, vpos)
ctx.stroke()
# Draw the volume level in the bar
ctx.set_source_rgba(*self._TEXT_RGBA)
textYOffset = (self._VH_HEIGHT - self._TEXT_HEIGHT) / 2
textXOffset = ((rect.width - (2 * self._VH_PADDING) - self._TEXT_WIDTH) / 2) + self._VH_PADDING
ctx.move_to(textXOffset, vpos + textYOffset)
ctx.set_font_size(self._FONT_SIZE)
text = "%d%%" % (self.mixerstrip.GetVolume() * 100)
ctx.show_text(text)
return False
#_____________________________________________________________________
def do_size_request(self, requisition):
"""
TODO
Parameters:
requisition -- TODO
"""
requisition.width = 100
requisition.height = -1
#_____________________________________________________________________
def Destroy(self):
"""
Deletes the cairo.ImageSurface and then calls the class destructor.
"""
del self.source
self.destroy()
#_____________________________________________________________________
def __GetVolumeHandleYPos(self):
"""
Calculates the verical position of the *center* of the volume handle
based on the instrument's current volume, and the size of the volume
handle itself (so that is doesn't go off the screen at the top or bottom.
Returns:
the Y value in pixels of the center of the volume handle.
"""
height = self.get_allocation().height
totalPixelHeight = height - self._VH_HEIGHT
inverseVolume = (self._MAX_VOLUME - self.mixerstrip.GetVolume()) / self._MAX_VOLUME
offset = self._VH_HEIGHT / 2
return totalPixelHeight * inverseVolume + offset
#_____________________________________________________________________
def __YPosOverVolumeHandle(self, yPos):
"""
Calculates if the given vertical position is located over top of the volume handle.
Parameters:
yPos -- the vertical position in pixels
Returns:
True if the value is over the volume handle; False otherwise.
"""
handleYPos = self.__GetVolumeHandleYPos()
halfHandle = self._VH_HEIGHT / 2
return handleYPos - halfHandle < yPos < handleYPos + halfHandle
#_____________________________________________________________________
#=========================================================================