# # 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() 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() # Reset the clip region ctx.reset_clip() # 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 #_____________________________________________________________________ #=========================================================================