import pygtk
pygtk.require( '2.0' )
import gtk

import gobject

from math import floor
import time

import common.Config as Config
from common.Config import imagefile
from Edit.NoteInterface import NoteInterface
from Edit.HitInterface import HitInterface
from Edit.MainWindow import CONTEXT

from common.Util.NoteDB import PARAMETER
from common.Util.CSoundNote import CSoundNote
from common.Generation.GenerationConstants import GenerationConstants
from common.Util.Profiler import TP

class SELECTNOTES:
    ALL = -1
    NONE = 0
    ADD = 1
    REMOVE = 2
    FLIP = 3
    EXCLUSIVE = 4

class INTERFACEMODE:
    DEFAULT = 0
    DRAW = 1
    PASTE_NOTES = 2
    PASTE_TRACKS = 3
    PAINT = 4

class TrackInterfaceParasite:
    def __init__( self, noteDB, owner, note ):
        if note.track == Config.NUMBER_OF_TRACKS-1: # drum track
            self.parasite = HitInterface( noteDB, owner, note )
        else:
            self.parasite = NoteInterface( noteDB, owner, note )

    def attach( self ):
        return self.parasite

class TrackInterface( gtk.EventBox ):

    def __init__( self, noteDB, owner, getScaleFunction, width ):
        gtk.EventBox.__init__( self )

        self.noteDB = noteDB
        self.owner = owner
        self.getScale = getScaleFunction

        self.drawingArea = gtk.DrawingArea()
        self.drawingAreaDirty = False # are we waiting to draw?
        self.add( self.drawingArea )
        self.dirtyRectToAdd = gtk.gdk.Rectangle() # used by the invalidate_rect function

        self.fullWidth = 1 # store the maximum allowed width
        self.width = 1
        self.height = 1

        self.interfaceMode = INTERFACEMODE.DEFAULT

        self.curPage = -1   # this isn't a real page at all!
        self.curBeats = 4
        self.painting = False
        self.pointerGrid = 1
        self.drawGrid = Config.DEFAULT_GRID
        self.paintGrid = Config.DEFAULT_GRID
        self.paintNoteDur = Config.DEFAULT_GRID

        self.selectedNotes = [ [] for i in range(Config.NUMBER_OF_TRACKS) ]

        self.curAction = False          # stores the current mouse action
        self.curActionObject = False    # stores the object that in handling the action

        self.lastDO = self.lastDP = self.lastDrumDP = self.lastDD = None

        self.clickButton = 0        # used in release and motion events to make sure we where actually the widget originally clicked. (hack for popup windows)
        self.buttonPressCount = 1   # used on release events to indicate double/triple releases
        self.clickLoc = [0,0]       # location of the last click
        self.marqueeLoc = False     # current drag location of the marquee
        self.marqueeRect = [[0,0],[0,0]]

        self.pasteTick = -1
        self.pasteTrack = -1
        self.pasteRect = False

        self.playheadT = 0
        self.playheadX = Config.TRACK_SPACING_DIV2

        self.cursor = { \
            "default":          None, \
            "drag-onset":       gtk.gdk.Cursor(gtk.gdk.SB_RIGHT_ARROW), \
            "drag-pitch":       gtk.gdk.Cursor(gtk.gdk.BOTTOM_SIDE), \
            "drag-duration":    gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE), \
            "drag-playhead":    gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW), \
            "pencil":           gtk.gdk.Cursor(gtk.gdk.PENCIL), \
            "paste":            gtk.gdk.Cursor(gtk.gdk.CENTER_PTR), \
            "error":            None }

        self.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK)

        self.connect( "size-allocate", self.size_allocate )

        self.drawingArea.connect( "expose-event", self.expose )
        self.connect( "button-press-event", self.handleButtonPress )
        self.connect( "button-release-event", self.handleButtonRelease )
        self.connect( "motion-notify-event", self.handleMotion )

        # prepare drawing stuff
        hexToInt = { "0":0, "1":1, "2":2, "3":3, "4":4, "5":5, "6":6, "7":7, "8":8, "9":9, "A":10, "B":11, "C":12, "D":13, "E":14, "F":15, "a":10, "b":11, "c":12, "d":13, "e":14, "f":15 }
        self.trackColors = []
        for i in Config.TRACK_COLORS:
            low = ( 256*(hexToInt[i[0][1]]*16+hexToInt[i[0][2]]), 256*(hexToInt[i[0][3]]*16+hexToInt[i[0][4]]), 256*(hexToInt[i[0][5]]*16+hexToInt[i[0][6]]) )
            high = ( 256*(hexToInt[i[1][1]]*16+hexToInt[i[1][2]]), 256*(hexToInt[i[1][3]]*16+hexToInt[i[1][4]]), 256*(hexToInt[i[1][5]]*16+hexToInt[i[1][6]]) )
            delta = ( high[0]-low[0], high[1]-low[1], high[2]-low[2] )
            self.trackColors.append( (low, delta) )

        colormap = self.drawingArea.get_colormap()
        self.beatColor = colormap.alloc_color( Config.BEAT_COLOR, True, True )
        self.playheadColor = colormap.alloc_color( Config.PLAYHEAD_COLOR, True, True )
        self.marqueeColor = colormap.alloc_color( Config.MARQUEE_COLOR, True, True )

        self.image = {}
        win = gtk.gdk.get_default_root_window()
        self.gc = gtk.gdk.GC( win )

        def prepareDrawable( name, width = -1 ):
            pix = gtk.gdk.pixbuf_new_from_file(imagefile(name + '.png'))
            if width != -1:
                pix = pix.scale_simple(width, pix.get_height(), gtk.gdk.INTERP_BILINEAR)
            self.image[name] = gtk.gdk.Pixmap( win, pix.get_width(), pix.get_height() )
            self.image[name].draw_pixbuf( self.gc, pix, 0, 0, 0, 0, pix.get_width(), pix.get_height(), gtk.gdk.RGB_DITHER_NONE )
        def preparePixbuf( name ):
            self.image[name] = gtk.gdk.pixbuf_new_from_file(imagefile(name + '.png'))

        prepareDrawable( "trackBG", width )
        prepareDrawable( "trackBGSelected", width )
        prepareDrawable( "trackBGDrum", width )
        prepareDrawable( "trackBGDrumSelected", width )
        preparePixbuf( "note" )
        preparePixbuf( "noteSelected" )
        preparePixbuf( "hit" )
        preparePixbuf( "hitSelected" )

        # define dimensions
        self.width = self.trackFullWidth = width
        self.trackWidth = self.width - Config.TRACK_SPACING
        self.trackFullHeight = self.image["trackBG"].get_size()[1]
        self.trackHeight = self.trackFullHeight - Config.TRACK_SPACING
        self.trackFullHeightDrum = self.image["trackBGDrum"].get_size()[1]
        self.trackHeightDrum = self.trackFullHeightDrum - Config.TRACK_SPACING
        self.height = self.trackHeight*(Config.NUMBER_OF_TRACKS-1) + self.trackHeightDrum + Config.TRACK_SPACING*Config.NUMBER_OF_TRACKS
        self.trackLimits = []
        self.trackRect = []
        self.drumIndex = Config.NUMBER_OF_TRACKS-1
        for i in range(self.drumIndex):
            start = i*(self.trackFullHeight)
            self.trackLimits.append( (start,start+self.trackFullHeight) )
            self.trackRect.append( gtk.gdk.Rectangle(Config.TRACK_SPACING_DIV2,start+Config.TRACK_SPACING_DIV2, self.trackWidth, self.trackHeight ) )
        self.trackLimits.append( ( self.height - self.trackFullHeightDrum, self.height ) )
        self.trackRect.append( gtk.gdk.Rectangle( Config.TRACK_SPACING_DIV2, self.height - self.trackFullHeightDrum + Config.TRACK_SPACING_DIV2, self.trackWidth, self.trackHeightDrum ) )

        self.pitchPerPixel = float(Config.NUMBER_OF_POSSIBLE_PITCHES-1) / (self.trackHeight - Config.NOTE_HEIGHT)
        self.pixelsPerPitch = float(self.trackHeight-Config.NOTE_HEIGHT)/(Config.MAXIMUM_PITCH - Config.MINIMUM_PITCH)
        self.pitchPerPixelDrum = float(Config.NUMBER_OF_POSSIBLE_PITCHES_DRUM-1)*Config.PITCH_STEP_DRUM / (self.trackHeightDrum - Config.HIT_HEIGHT)
        self.pixelsPerPitchDrum = float(self.trackHeightDrum-Config.HIT_HEIGHT)/(Config.MAXIMUM_PITCH_DRUM - Config.MINIMUM_PITCH_DRUM )

        self.pixelsPerTick = [0] + [ self.trackWidth/float(i*Config.TICKS_PER_BEAT) for i in range(1,Config.MAXIMUM_BEATS+1) ]

        self.ticksPerPixel = [0] + [ 1.0/self.pixelsPerTick[i] for i in range(1,Config.MAXIMUM_BEATS+1) ]

        self.beatSpacing = [[0]]
        for i in range(1,Config.MAXIMUM_BEATS+1):
            self.beatSpacing.append( [ self.ticksToPixels( i, Config.TICKS_PER_BEAT*j ) for j in range(i) ] )

        # screen buffers
        self.screenBuf = [ gtk.gdk.Pixmap( win, self.width, self.height ), \
                           gtk.gdk.Pixmap( win, self.width, self.height ) ]
        self.screenBufPage = [ -1, -1 ]
        self.screenBufBeats = [ -1, -1 ]
        self.screenBufDirtyRect =  [ gtk.gdk.Rectangle(), gtk.gdk.Rectangle() ]
        self.screenBufDirty = [ False, False ]
        self.screenBufResume = [ [0,0], [0,0] ] # allows for stopping and restarting in the middle of a draw
        self.curScreen = 0
        self.preScreen = 1
        self.predrawTimeout = False

    #-- private --------------------------------------------

    def _updateClipboardArea( self ):
        self.clipboardArea = self.owner.getClipboardArea( self.curPage )
        self.clipboardTrackTop = 0
        for t in range(self.drumIndex):
            if self.clipboardArea["tracks"][t]: break
            self.clipboardTrackTop += 1
        self.clipboardDrumTrack = self.clipboardArea["tracks"][self.drumIndex]

    #=======================================================
    # NoteDB notifications

    def notifyPageAdd( self, id, at ):
        return

    def notifyPageDelete( self, which, safe ):
        if self.screenBufPage[self.preScreen] in which:
            self.screenBufPage[self.preScreen] = -1

    def notifyPageDuplicate( self, new, at ):
        return

    def notifyPageMove( self, which, low, high ):
        return

    def notifyPageUpdate( self, page, parameter, value ):
        if parameter == PARAMETER.PAGE_BEATS:
            notes = self.noteDB.getNotesByPage( page, self )
            for note in notes:
                note.updateTransform()

            if page == self.screenBufPage[self.curScreen]:
                self.screenBufBeats[self.curScreen] = value
                self.curBeats = value
                if self.playheadT >= value*Config.TICKS_PER_BEAT:
                    self.playheadT = value*Config.TICKS_PER_BEAT - 1
                self.playheadX = self.ticksToPixels( self.curBeats, self.playheadT ) + Config.TRACK_SPACING_DIV2
                self.invalidate_rect( 0, 0, self.width, self.height, page )
            if page == self.screenBufPage[self.preScreen]:
                self.screenBufBeats[self.preScreen] = value
                self.invalidate_rect( 0, 0, self.width, self.height, page )
                self.predrawPage()


    #=======================================================
    #  Module Interface

    def getDrawingPackage( self, track ):
        if track == self.drumIndex:
            return ( self.image["hit"], self.image["hitSelected"], self.drawingArea.get_colormap(), self.trackColors[track] )
        else:
            return ( self.image["note"], self.image["noteSelected"], self.drawingArea.get_colormap(), self.trackColors[track] )

    def getActivePages( self ):
        return self.screenBufPage

    def setPredrawPage( self, page ):
        if self.screenBufPage[self.preScreen] != page:
            self.screenBufPage[self.preScreen] = page
            self.screenBufBeats[self.preScreen] = self.noteDB.getPage(page).beats
            self.invalidate_rect( 0, 0, self.width, self.height, page )
            return True
        return False

    def predrawPage( self ):
        if self.screenBufPage[self.preScreen] == -1: return True # no page to predraw
        if not self.predrawTimeout:
            self.predrawTimeout = gobject.timeout_add( 50, self._predrawTimeout )

    def abortPredrawPage( self ):
        if self.predrawTimeout:
            gobject.source_remove( self.predrawTimeout )
            self.predrawTimeout = False

    def _predrawTimeout( self ):
        if self.preScreen == -1: return False # no page to predraw
        if self.draw( self.preScreen, False, time.time() + 0.020 ): # 20 ms time limit
            self.predrawTimeout = False
            return False
        return True



    def displayPage( self, page, predraw = -1 ):
        if page == self.curPage:
            if predraw >= 0 and self.screenBufPage[self.preScreen] != predraw:
                self.screenBufPage[self.preScreen] = predraw
                self.screenBufBeats[self.preScreen] = self.noteDB.getPage(predraw).beats
                self.invalidate_rect( 0, 0, self.width, self.height, predraw )
            return

        if self.curPage >= 0 and self.curPage != page: clearNotes = True
        else: clearNotes = False

        oldPage = self.curPage
        self.curPage = page
        self.curBeats = self.noteDB.getPage(page).beats

        if self.screenBufPage[self.preScreen] == self.curPage: # we predrew this page, so smart!
            t = self.preScreen
            self.preScreen = self.curScreen
            self.curScreen = t
            self.invalidate_rect( 0, 0, self.width, self.height, self.curPage, False )
        else: # we need to draw this page from scratch
            self.screenBufPage[self.curScreen] = self.curPage
            self.screenBufBeats[self.curScreen] = self.curBeats
            self.invalidate_rect( 0, 0, self.width, self.height, self.curPage )

        if predraw >= 0 and self.screenBufPage[self.preScreen] != predraw:
            self.screenBufPage[self.preScreen] = predraw
            self.screenBufBeats[self.preScreen] = self.noteDB.getPage(predraw).beats
            self.invalidate_rect( 0, 0, self.width, self.height, predraw )

        if clearNotes: # clear the notes now that we've sorted out the screen buffers
            self.clearSelectedNotes( oldPage )

        if self.curAction == "paste":
            self._updateClipboardArea()

    def getPlayhead( self ):
        return self.playheadT

    def setPlayhead( self, ticks ):
        if self.playheadT != ticks:
            self.invalidate_rect( self.playheadX-Config.PLAYHEAD_SIZE/2, 0, Config.PLAYHEAD_SIZE, self.height, self.curPage, False )
            self.playheadX = self.ticksToPixels( self.curBeats, ticks ) + Config.TRACK_SPACING_DIV2
            self.invalidate_rect( self.playheadX-Config.PLAYHEAD_SIZE/2, 0, Config.PLAYHEAD_SIZE, self.height, self.curPage, False )
            self.playheadT = ticks

    def setInterfaceMode( self, mode ):
        self.doneCurrentAction()

        if mode == "tool":
            mode = self.owner.getTool()

        if mode == "draw":
            self.interfaceMode = INTERFACEMODE.DRAW
        elif mode == "paint":
            self.interfaceMode = INTERFACEMODE.PAINT
        elif mode == "paste_notes":
            self.interfaceMode = INTERFACEMODE.PASTE_NOTES
            self.setCurrentAction("paste", self)
        elif mode == "paste_tracks":
            self.interfaceMode = INTERFACEMODE.PASTE_TRACKS
            self.setCurrentAction("paste", self )
        else:
            self.interfaceMode = INTERFACEMODE.DEFAULT

    def getSelectedNotes( self ):
        ids = []
        for t in range(Config.NUMBER_OF_TRACKS):
            ids.append( [ n.note.id for n in self.selectedNotes[t] ] )
        return ids

    #=======================================================
    #  Event Callbacks

    def size_allocate( self, widget, allocation ):
        self.alloc = allocation
        width = allocation.width
        height = allocation.height

        self.drawingArea.set_size_request( width, height )

        if self.window != None:
            self.invalidate_rect( 0, 0, width, height, self.curPage, False )

    def setPointerGrid(self, value):
        self.pointerGrid = value

    def setDrawGrid(self, value):
        self.drawGrid = value

    def setPaintGrid(self, value):
        self.paintGrid = value

    def setPaintNoteDur(self, value):
        self.paintNoteDur = value

    def handleButtonPress( self, widget, event ):

        TP.ProfileBegin( "TI::handleButtonPress" )

        self.clickButton = event.button

        if event.type == gtk.gdk._2BUTTON_PRESS:   self.buttonPressCount = 2
        elif event.type == gtk.gdk._3BUTTON_PRESS: self.buttonPressCount = 3
        else:                                      self.buttonPressCount = 1

        self.clickLoc = [ int(event.x), int(event.y) ]


        if self.curAction == "paste":
            self.doPaste()
            self.setCurrentAction("block-track-select")
            TP.ProfileEnd( "TI::handleButtonPress" )
            return


        # check if we clicked on the playhead
        if event.x >= self.playheadX and event.x <= self.playheadX + Config.PLAYHEAD_SIZE:
            self.setCurrentAction( "playhead-drag", self )
            TP.ProfileEnd( "TI::handleButtonPress" )
            return

        if event.x < Config.TRACK_SPACING_DIV2 or event.x > self.trackWidth + Config.TRACK_SPACING_DIV2:
            TP.ProfileEnd( "TI::handleButtonPress" )
            return

        for i in range(Config.NUMBER_OF_TRACKS):
            if self.trackLimits[i][0] > event.y: break
            if self.trackLimits[i][1] < event.y: continue

            handled = 0
            notes = self.noteDB.getNotesByTrack( self.curPage, i, self )
            last = len(notes)-1
            for n in range(last+1):
                if i == self.drumIndex and n < last: # check to see if the next hit overlaps this one
                    if notes[n].getStartTick() == notes[n+1].getStartTick() and notes[n].getPitch() == notes[n+1].getPitch():
                        continue
                handled = notes[n].handleButtonPress( self, event )
                if handled == 0:
                    continue
                elif handled == 1:
                    if not self.curAction: self.curAction = True # it was handled but no action was declared, set curAction to True anyway
                    TP.ProfileEnd( "TI::handleButtonPress" )
                    return
                else:      # all other options mean we can stop looking
                    break

            if self.interfaceMode == INTERFACEMODE.DRAW:
                if not handled or handled == -1:  # event didn't overlap any notes, so we can draw
                    if i == self.drumIndex: pitch = min( self.pixelsToPitchDrumFloor( self.clickLoc[1] - self.trackLimits[i][1] + Config.HIT_HEIGHT//2 )//Config.PITCH_STEP_DRUM, Config.NUMBER_OF_POSSIBLE_PITCHES_DRUM-1)*Config.PITCH_STEP_DRUM + Config.MINIMUM_PITCH_DRUM
                    else:  pitch = min( self.pixelsToPitchFloor( self.clickLoc[1] - self.trackLimits[i][1] + Config.NOTE_HEIGHT//2 ), Config.NUMBER_OF_POSSIBLE_PITCHES-1) + Config.MINIMUM_PITCH
                    onset = self.pixelsToTicksFloor( self.curBeats, self.clickLoc[0] - self.trackRect[i].x)
                    snapOnset = self.drawGrid * int(onset / float(self.drawGrid) + 0.5)
                    cs = CSoundNote( snapOnset,
                                     pitch,
                                     0.75,
                                     0.5,
                                     1,
                                     i,
                                     instrumentId = self.owner.getTrackInstrument(i).instrumentId )
                    cs.pageId = self.curPage
                    id = self.noteDB.addNote( -1, self.curPage, i, cs )
                    n = self.noteDB.getNote( self.curPage, i, id, self )
                    self.selectNotes( { i:[n] }, True )
                    n.playSampleNote( False )

                    noteS = self.noteDB.getNotesByTrack(self.curPage, i)
                    for note in noteS:
                        if note.cs.onset < snapOnset and (note.cs.onset + note.cs.duration) > snapOnset:
                            self.noteDB.updateNote(self.curPage, i, note.id, PARAMETER.DURATION, snapOnset - note.cs.onset)

                    if i != self.drumIndex: # switch to drag duration
                        self.updateDragLimits()
                        self.clickLoc[0] += self.ticksToPixels( self.curBeats, 1 )
                        self.setCurrentAction( "note-drag-duration", n )
                        self.setCursor("drag-duration")
                    else:
                        self.curAction = True # we handled this, but there's no real action

                    TP.ProfileEnd( "TI::handleButtonPress" )
                    return
            elif self.interfaceMode == INTERFACEMODE.PAINT:
                self.scale = self.getScale()
                self.painting = True
                self.paintTrack = i
                if i == self.drumIndex:
                    pitch = min( self.pixelsToPitchDrumFloor( self.clickLoc[1] - self.trackLimits[i][1] + Config.HIT_HEIGHT//2 )//Config.PITCH_STEP_DRUM, Config.NUMBER_OF_POSSIBLE_PITCHES_DRUM-1)*Config.PITCH_STEP_DRUM + Config.MINIMUM_PITCH_DRUM
                    if pitch < 24:
                        pitch = 24
                    elif pitch > 48:
                        pitch = 48
                    else:
                        pitch = pitch
                else:
                    pitch = min( self.pixelsToPitchFloor( self.clickLoc[1] - self.trackLimits[i][1] + Config.NOTE_HEIGHT//2 ), Config.NUMBER_OF_POSSIBLE_PITCHES-1) + Config.MINIMUM_PITCH
                    if pitch < 24:
                        pitch = 24
                    elif pitch > 48:
                        pitch = 48
                    else:
                        pitch = pitch

                    minDiff = 100
                    for pit in GenerationConstants.SCALES[self.scale]:
                        diff = abs(pitch-(pit+36))
                        if diff < minDiff:
                            minDiff = diff
                            nearestPit = pit
                    pitch = nearestPit+36

                onset = self.pixelsToTicksFloor( self.curBeats, self.clickLoc[0] - self.trackRect[i].x )
                onset = self.paintGrid * int(onset / self.paintGrid + 0.5)
                self.pLastPos = onset
                if i != self.drumIndex:
                    noteS = self.noteDB.getNotesByTrack(self.curPage, i)
                    ids = []
                    stream = []
                    for n in noteS:
                        if n.cs.onset >= onset and n.cs.onset < (onset + self.paintNoteDur):
                            ids.append(n.id)
                        if onset > n.cs.onset and onset < (n.cs.onset + n.cs.duration):
                            ids.append(n.id)
                    if len(ids):
                        stream += [self.curPage, i, len(ids)] + ids
                        self.noteDB.deleteNotes( stream + [-1] )

                cs = CSoundNote( int(onset),
                                     pitch,
                                     0.75,
                                     0.5,
                                     1,
                                     i,
                                     instrumentId = self.owner.getTrackInstrument(i).instrumentId )
                cs.pageId = self.curPage
                id = self.noteDB.addNote( -1, self.curPage, i, cs )
                self.noteDB.updateNote(self.curPage, i, id, PARAMETER.DURATION, self.paintNoteDur)
                n = self.noteDB.getNote( self.curPage, i, id, self )
                self.selectNotes( { i:[n] }, True )
                n.playSampleNote( False )
                self.curAction = True

        TP.ProfileEnd( "TI::handleButtonPress" )


    def handleButtonRelease( self, widget, event ):
        if not self.clickButton: return # we recieved this event but were never clicked! (probably a popup window was open)
        self.clickButton = 0
        self.painting = False

        TP.ProfileBegin( "TI::handleButtonRelease" )

        if event.button != 1:
            TP.ProfileEnd( "TI::handleButtonRelease" )
            return

        if not self.curAction: #do track selection stuff here so that we can also handle marquee selection
            for i in range(Config.NUMBER_OF_TRACKS):
                if self.trackLimits[i][0] > event.y: break
                if self.trackLimits[i][1] < event.y: continue
                if event.button == 1:
                    if self.buttonPressCount == 1:   self.owner.toggleTrack( i, False )
                    elif self.buttonPressCount == 2: self.owner.toggleTrack( i, True )
                    else:                            self.owner.clearTracks()
                break

            TP.ProfileEnd( "TI::handleButtonRelease" )
            return

        if not self.curActionObject: # there was no real action to carry out
            self.curAction = False
            TP.ProfileEnd( "TI::handleButtonRelease" )
            return

        if self.curActionObject != self:
            self.curActionObject.handleButtonRelease( self, event, self.buttonPressCount )
            self.updateTooltip( event )
        else:
            # we're doing the action ourselves
            if self.curAction == "marquee":         self.doneMarquee( event )
            elif self.curAction == "playhead-drag": self.donePlayhead( event )
            self.updateTooltip( event )


        TP.ProfileEnd( "TI::handleButtonRelease" )
        return

    def handleMotion( self, widget, event ):
        TP.ProfileBegin( "TI::handleMotion::Common" )

        if event.is_hint:
            x, y, state = self.window.get_pointer()
            event.x = float(x)
            event.y = float(y)
            event.state = state

        if self.painting:
            i = self.paintTrack
            curPos = self.pixelsToTicksFloor(self.curBeats, event.x - self.trackRect[i].x)
            gridPos = self.paintGrid * int(curPos / self.paintGrid + 0.5)
            if gridPos >= self.curBeats * Config.TICKS_PER_BEAT:
                return
            if gridPos != self.pLastPos:
                self.pLastPos = gridPos
                if i == self.drumIndex:
                    pitch = min( self.pixelsToPitchDrumFloor( int(event.y) - self.trackLimits[i][1] + Config.HIT_HEIGHT//2 )//Config.PITCH_STEP_DRUM, Config.NUMBER_OF_POSSIBLE_PITCHES_DRUM-1)*Config.PITCH_STEP_DRUM + Config.MINIMUM_PITCH_DRUM
                    if pitch < 24:
                        pitch = 24
                    elif pitch > 48:
                        pitch = 48
                    else:
                        pitch = pitch
                else:
                    pitch = min( self.pixelsToPitchFloor( int(event.y) - self.trackLimits[i][1] + Config.NOTE_HEIGHT//2 ), Config.NUMBER_OF_POSSIBLE_PITCHES-1) + Config.MINIMUM_PITCH
                    if pitch < 24:
                        pitch = 24
                    elif pitch > 48:
                        pitch = 48
                    else:
                        pitch = pitch
                    minDiff = 100
                    for pit in GenerationConstants.SCALES[self.scale]:
                        diff = abs(pitch-(pit+36))
                        if diff < minDiff:
                            minDiff = diff
                            nearestPit = pit
                    pitch = nearestPit+36

                onset = gridPos
                if i != self.drumIndex:
                    noteS = self.noteDB.getNotesByTrack(self.curPage, i)
                    ids = []
                    stream = []
                    for n in noteS:
                        if n.cs.onset >= onset and n.cs.onset < (onset + self.paintNoteDur):
                            ids.append(n.id)
                        if onset > n.cs.onset and onset < (n.cs.onset + n.cs.duration):
                            ids.append(n.id)
                    if len(ids):
                        stream += [self.curPage, i, len(ids)] + ids
                        self.noteDB.deleteNotes( stream + [-1] )

                cs = CSoundNote( int(onset),
                                     pitch,
                                     0.75,
                                     0.5,
                                     1,
                                     i,
                                     instrumentId = self.owner.getTrackInstrument(i).instrumentId )
                cs.pageId = self.curPage
                id = self.noteDB.addNote( -1, self.curPage, i, cs )
                self.noteDB.updateNote(self.curPage, i, id, PARAMETER.DURATION, self.paintNoteDur)
                n = self.noteDB.getNote( self.curPage, i, id, self )
                self.selectNotes( { i:[n] }, True )
                n.playSampleNote( False )
                self.curAction = True


        TP.ProfileEnd( "TI::handleMotion::Common" )

        if not self.clickButton and self.curAction != "paste": # we recieved this event but were never clicked! (probably a popup window was open)
            TP.ProfileBegin( "TI::handleMotion::Hover" )
            self.updateTooltip( event )
            TP.ProfileEnd( "TI::handleMotion::Hover" )
            return

        if self.curAction == "paste":
            TP.ProfileBegin( "TI::handleMotion::Paste" )
            top = Config.NUMBER_OF_TRACKS
            for i in range(Config.NUMBER_OF_TRACKS):
                if self.trackLimits[i][0] > event.y: break
                if self.trackLimits[i][1] < event.y: continue
                top = i
                break
            self.updatePaste( self.pixelsToTicksFloor( self.curBeats, event.x ), top )
            TP.ProfileEnd( "TI::handleMotion::Paste" )
        elif event.state & gtk.gdk.BUTTON1_MASK:
            TP.ProfileBegin( "TI::handleMotion::Drag" )

            if not self.curAction: # no action is in progress yet we're dragging, start a marquee
                self.setCurrentAction( "marquee", self )

            if self.curAction == "note-drag-onset":
                self.noteDragOnset( event )

            elif self.curAction == "note-drag-duration":
                self.noteDragDuration( event )

            elif self.curAction == "note-drag-pitch":
                self.noteDragPitch( event )

            elif self.curAction == "note-drag-pitch-drum":
                self.noteDragPitch( event, True )

            elif self.curAction == "marquee":
                self.updateMarquee( event )

            elif self.curAction == "playhead-drag":
                self.updatePlayhead( event )

            TP.ProfileEnd( "TI::handleMotion::Drag" )
        else:
            TP.ProfileBegin( "TI::handleMotion::Hover" )
            self.updateTooltip( event )
            TP.ProfileEnd( "TI::handleMotion::Hover" )

        return

    #=======================================================
    #  Actions

    def setCurrentAction( self, action, obj = None ):
        if self.curAction:
            self.doneCurrentAction()

        self.curAction = action
        self.curActionObject = obj

        if   action == "note-drag-onset":      self.updateDragLimits()
        elif action == "note-drag-duration":   self.updateDragLimits()
        elif action == "note-drag-pitch":      self.updateDragLimits()
        elif action == "note-drag-pitch-drum": self.updateDragLimits()
        elif action == "paste":
            self._updateClipboardArea()
            self.setCursor("paste")

    def doneCurrentAction( self ):
        if not self.curAction: return
        action = self.curAction
        self.curAction = False

        if   action == "note-drag-onset":      self.doneNoteDrag( action )
        elif action == "note-drag-duration":   self.doneNoteDrag( action )
        elif action == "note-drag-pitch":      self.doneNoteDrag( action )
        elif action == "note-drag-pitch-drum": self.doneNoteDrag( action )
        elif action == "paste":
            self.owner.cleanupClipboard()

    def trackToggled( self, trackN = -1 ):
        if trackN == -1: self.invalidate_rect( 0, 0, self.width, self.height )
        else: self.invalidate_rect( 0, self.trackLimits[trackN][0], self.width, self.trackLimits[trackN][1]-self.trackLimits[trackN][0] )

    def selectionChanged( self ):
        if   self.curAction == "note-drag-onset":      self.updateDragLimits()
        elif self.curAction == "note-drag-duration":   self.updateDragLimits()
        elif self.curAction == "note-drag-pitch":      self.updateDragLimits()
        elif self.curAction == "note-drag-pitch-drum": self.updateDragLimits()
        for i in range(Config.NUMBER_OF_TRACKS):
            if len(self.selectedNotes[i]):
                self.owner.setContextState( CONTEXT.NOTE, True )
                self.owner.setContext( CONTEXT.NOTE )
                return
        self.owner.setContextState( CONTEXT.NOTE, False )

    def applyNoteSelection( self, mode, trackN, which, page = -1 ):
        if page == -1: page = self.curPage
        if mode == SELECTNOTES.ALL:
            track = self.noteDB.getNotesByTrack( page, trackN, self )
            map( lambda note:note.setSelected( True ), track )
            self.selectedNotes[trackN] = []
            map( lambda note:self.selectedNotes[trackN].append(note), track )
        elif mode == SELECTNOTES.NONE:
            track = self.selectedNotes[trackN] #self.noteDB.getNotesByTrack( page, trackN, self )
            map( lambda note:note.setSelected( False ), track )
            self.selectedNotes[trackN] = []
        elif mode == SELECTNOTES.ADD:
            for note in which:
                if note.setSelected( True ):
                    self.selectedNotes[trackN].append( note )
        elif mode == SELECTNOTES.REMOVE:
            for note in which:
                if note.setSelected( False ):
                    self.selectedNotes[trackN].remove( note )
        elif mode == SELECTNOTES.FLIP:
            for note in which:
                if note.getSelected():
                    note.setSelected( False )
                    self.selectedNotes[trackN].remove( note )
                else:
                    note.setSelected( True )
                    self.selectedNotes[trackN].append( note )
        elif mode == SELECTNOTES.EXCLUSIVE:
            notes = self.noteDB.getNotesByTrack( page, trackN, self )
            for n in range(len(notes)):
                if notes[n] in which:
                    if notes[n].setSelected( True ):
                        self.selectedNotes[trackN].append( notes[n] )
                else:
                    if notes[n].setSelected( False ):
                        self.selectedNotes[trackN].remove( notes[n] )

    def selectNotesByBar( self, trackN, start, stop, page = -1 ):
        for i in range(Config.NUMBER_OF_TRACKS):
            if i == trackN:
                notes = []
                track = self.noteDB.getNotesByTrack( self.curPage, trackN, self )
                for n in range(len(track)):
                    if track[n].testOnset( start, stop ): notes.append(track[n])
                if not Config.ModKeys.ctrlDown: self.applyNoteSelection( SELECTNOTES.EXCLUSIVE, trackN, notes, page )
                else:                           self.applyNoteSelection( SELECTNOTES.ADD, trackN, notes, page )
            else:
                if not Config.ModKeys.ctrlDown: self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
        self.selectionChanged()

    def selectNotesByTrack( self, trackN, page = -1 ):
        if Config.ModKeys.ctrlDown:
            self.applyNoteSelection( SELECTNOTES.ALL, trackN, [], page )
        else:
            for i in range(Config.NUMBER_OF_TRACKS):
                if i == trackN: self.applyNoteSelection( SELECTNOTES.ALL, trackN, [], page )
                else:           self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
        self.selectionChanged()

    def selectNotes( self, noteDic, ignoreCtrl = False, page = -1 ):
        if Config.ModKeys.ctrlDown and not ignoreCtrl:
            for i in noteDic:
                self.applyNoteSelection( SELECTNOTES.FLIP, i, noteDic[i], page )
        else:
            for i in range(Config.NUMBER_OF_TRACKS):
                if i in noteDic: self.applyNoteSelection( SELECTNOTES.EXCLUSIVE, i, noteDic[i], page )
                else:            self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
        self.selectionChanged()

    def deselectNotes( self, noteDic, page = -1 ):
        for i in noteDic:
            self.applyNoteSelection( SELECTNOTES.REMOVE, i, noteDic[i], page )
        self.selectionChanged()

    def clearSelectedNotes( self, page = -1 ):
        for i in range(Config.NUMBER_OF_TRACKS):
            self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
        self.selectionChanged()

    def updateDragLimits( self ):
        self.dragLimits = [ [-9999,9999], [-9999,9999], [-9999,9999] ] # initialize to big numbers!
        maxRightBound = self.noteDB.getPage(self.curPage).ticks

        for i in range(Config.NUMBER_OF_TRACKS):
            if not len(self.selectedNotes[i]): continue  # no selected notes here

            track = self.noteDB.getNotesByTrack( self.curPage, i, self )
            leftBound = 0
            skip = True # skip the first note
            for n in range(len(track)):
                if skip:
                    skip = False
                    thisNote = track[n]
                    continue
                nextNote = track[n]
                if not thisNote.getSelected():
                    leftBound = thisNote.getEndTick()
                else:
                    if not nextNote.getSelected():
                        rightBound = min( nextNote.getStartTick(), maxRightBound )
                        widthBound = rightBound
                    else:
                        rightBound = maxRightBound
                        widthBound = min( nextNote.getStartTick(), maxRightBound )
                    thisNote.updateDragLimits( self.dragLimits, leftBound, rightBound, widthBound, maxRightBound )
                thisNote = nextNote
            # do the last note
            if thisNote.getSelected():
                thisNote.updateDragLimits( self.dragLimits, leftBound, maxRightBound, maxRightBound, maxRightBound )

    def noteDragOnset( self, event ):
        do = self.pixelsToTicks( self.curBeats, event.x - self.clickLoc[0] )
        do = min( self.dragLimits[0][1], max( self.dragLimits[0][0], do ) )
        do = self.pointerGrid * int(do / self.pointerGrid)

        if do != self.lastDO:
            self.lastDO = do
            stream = []
            for i in range(Config.NUMBER_OF_TRACKS):
                tstream = []
                for note in self.selectedNotes[i]:
                    note.noteDragOnset( do, tstream )
                if len(tstream):
                    stream += [ self.curPage, i, PARAMETER.ONSET, len(tstream)//2 ] + tstream
            if len(stream):
                self.noteDB.updateNotes( stream + [-1] )

    def noteDragDuration( self, event ):
        dd = self.pixelsToTicks( self.curBeats, event.x - self.clickLoc[0] )
        dd = min( self.dragLimits[2][1], max( self.dragLimits[2][0], dd ) )

        if dd != self.lastDD:
            self.lastDD = dd
            stream = []
            for i in range(Config.NUMBER_OF_TRACKS):
                tstream = []
                for note in self.selectedNotes[i]:
                    note.noteDragDuration( dd, tstream )
                if len(tstream):
                    stream += [ self.curPage, i, PARAMETER.DURATION, len(tstream)//2 ] + tstream
            if len(stream):
                self.noteDB.updateNotes( stream + [-1] )

    def noteDragPitch( self, event, drum = False ):
        if not drum: dp = self.pixelsToPitch( event.y - self.clickLoc[1] )
        else: dp = self.pixelsToPitchDrum( event.y - self.clickLoc[1] )
        dp = min( self.dragLimits[1][1], max( self.dragLimits[1][0], dp ) )

        if dp != self.lastDP:
            self.lastDP = dp
            stream = []
            for i in range(Config.NUMBER_OF_TRACKS):
                tstream = []
                for note in self.selectedNotes[i]:
                    note.noteDragPitch( dp, tstream )
                if len(tstream):
                    stream += [ self.curPage, i, PARAMETER.PITCH, len(tstream)//2 ] + tstream
            if len(stream):
                self.noteDB.updateNotes( stream + [-1] )

            if self.curActionObject.note.track != self.drumIndex:
                self.curActionObject.playSampleNote( True )
            elif dp != self.lastDrumDP and not dp%2: # only play of "full" drum pitches
                self.lastDrumDP = dp
                self.curActionObject.playSampleNote( False )

    def doneNoteDrag( self, action ):
       # if action == "note-drag-pitch" or action == "note-drag-pitch-drum":
       #     self.curActionObject.playSampleNote()

        self.lastDO = self.lastDP = self.lastDrumDP = self.lastDD = None

        for i in range(Config.NUMBER_OF_TRACKS):
            for note in self.selectedNotes[i]:
                note.doneNoteDrag( self )

    def noteStepOnset( self, step ):
        stream = []
        for i in range(Config.NUMBER_OF_TRACKS):
            if not len(self.selectedNotes[i]): continue  # no selected notes here

            tstream = []
            track = self.noteDB.getNotesByTrack( self.curPage, i, self )
            if step < 0: # moving to the left, iterate forwards
                leftBound = 0
                for n in range(len(track)):
                    leftBound = track[n].noteDecOnset( step, leftBound, tstream )
            else:        # moving to the right, iterate backwards
                rightBound = self.noteDB.getPage(self.curPage).ticks
                for n in range(len(track)-1, -1, -1 ):
                    rightBound = track[n].noteIncOnset( step, rightBound, tstream )

            if len(tstream):
                stream += [ self.curPage, i, PARAMETER.ONSET, len(tstream)//2 ] + tstream

        if len(stream):
            self.noteDB.updateNotes( stream + [-1] )

    def noteStepPitch( self, step ):
        stream = []
        for i in range(Config.NUMBER_OF_TRACKS):
            if not len(self.selectedNotes[i]): continue  # no selected notes here

            tstream = []
            if step < 0:
                for n in self.selectedNotes[i]:
                    n.noteDecPitch( step, tstream )
            else:
                for n in self.selectedNotes[i]:
                    n.noteIncPitch( step, tstream )

            if len(tstream):
                stream += [ self.curPage, i, PARAMETER.PITCH, len(tstream)//2 ] + tstream

        if len(stream):
            self.noteDB.updateNotes( stream + [-1] )

    def noteStepDuration( self, step ):
        stream = []
        for i in range(Config.NUMBER_OF_TRACKS):
            if not len(self.selectedNotes[i]): continue  # no selected notes here

            tstream = []
            if step < 0:
                for n in self.selectedNotes[i]:
                    n.noteDecDuration( step, tstream )
            else:
                track = self.noteDB.getNotesByTrack( self.curPage, i, self )
                for j in range(len(track)-1):
                    track[j].noteIncDuration( step, track[j+1].getStartTick(), tstream )
                track[len(track)-1].noteIncDuration( step, self.noteDB.getPage(self.curPage).ticks, tstream )

            if len(tstream):
                stream += [ self.curPage, i, PARAMETER.DURATION, len(tstream)//2 ] + tstream

        if len(stream):
            self.noteDB.updateNotes( stream + [-1] )

    def noteStepVolume( self, step ):
        stream = []
        for i in range(Config.NUMBER_OF_TRACKS):
            if not len(self.selectedNotes[i]): continue  # no selected notes here

            tstream = []
            if step < 0:
                for n in self.selectedNotes[i]:
                    n.noteDecVolume( step, tstream )
            else:
                for n in self.selectedNotes[i]:
                    n.noteIncVolume( step, tstream )

            if len(tstream):
                stream += [ self.curPage, i, PARAMETER.AMPLITUDE, len(tstream)//2 ] + tstream

        if len(stream):
            self.noteDB.updateNotes( stream + [-1] )


    def updateMarquee( self, event ):
        if self.marqueeLoc:
            oldX = self.marqueeRect[0][0]
            oldEndX = self.marqueeRect[0][0] + self.marqueeRect[1][0]
            oldY = self.marqueeRect[0][1]
            oldEndY = self.marqueeRect[0][1] + self.marqueeRect[1][1]
        else:
            oldX = oldEndX = self.clickLoc[0]
            oldY = oldEndY = self.clickLoc[1]

        self.marqueeLoc = [ int(event.x), int(event.y) ]
        if self.marqueeLoc[0] < 0: self.marqueeLoc[0] = 0
        elif self.marqueeLoc[0] > self.width: self.marqueeLoc[0] = self.width
        if self.marqueeLoc[1] < 0: self.marqueeLoc[1] = 0
        elif self.marqueeLoc[1] > self.height: self.marqueeLoc[1] = self.height

        if self.marqueeLoc[0] > self.clickLoc[0]:
            self.marqueeRect[0][0] = self.clickLoc[0]
            self.marqueeRect[1][0] = self.marqueeLoc[0] - self.clickLoc[0]
        else:
            self.marqueeRect[0][0] = self.marqueeLoc[0]
            self.marqueeRect[1][0] = self.clickLoc[0] - self.marqueeLoc[0]
        if self.marqueeLoc[1] > self.clickLoc[1]:
            self.marqueeRect[0][1] = self.clickLoc[1]
            self.marqueeRect[1][1] = self.marqueeLoc[1] - self.clickLoc[1]
        else:
            self.marqueeRect[0][1] = self.marqueeLoc[1]
            self.marqueeRect[1][1] = self.clickLoc[1] - self.marqueeLoc[1]

        x = min( self.marqueeRect[0][0], oldX )
        width = max( self.marqueeRect[0][0] + self.marqueeRect[1][0], oldEndX ) - x
        y = min( self.marqueeRect[0][1], oldY )
        height = max( self.marqueeRect[0][1] + self.marqueeRect[1][1], oldEndY ) - y
        self.invalidate_rect( x-1, y-1, width+2, height+2, self.curPage, False )

    def doneMarquee( self, event ):
        if self.marqueeLoc:
            stop =  [ self.marqueeRect[0][0] + self.marqueeRect[1][0], self.marqueeRect[0][1] + self.marqueeRect[1][1] ]

            select = {}

            for i in range(Config.NUMBER_OF_TRACKS):
                intersectionY = [ max(self.marqueeRect[0][1],self.trackLimits[i][0]), min(stop[1],self.trackLimits[i][1]) ]
                if intersectionY[0] > intersectionY[1]:
                    continue

                notes = []
                track = self.noteDB.getNotesByTrack( self.curPage, i, self )
                for n in range(len(track)):
                    hit = track[n].handleMarqueeSelect( self,
                                      [ self.marqueeRect[0][0], intersectionY[0] ], \
                                      [ stop[0], intersectionY[1] ] )
                    if hit: notes.append(track[n])

                if len(notes): select[i] = notes

            self.selectNotes( select )

        self.marqueeLoc = False
        self.doneCurrentAction()

        self.invalidate_rect( self.marqueeRect[0][0]-1, self.marqueeRect[0][1]-1, self.marqueeRect[1][0]+2, self.marqueeRect[1][1]+2, self.curPage, False )

    def updatePlayhead( self, event ):
        x = min( self.trackWidth - self.pixelsPerTick[self.curBeats], max( Config.TRACK_SPACING_DIV2, event.x ) )
        self.setPlayhead( self.pixelsToTicks( self.curBeats, x ) )

    def donePlayhead( self, event ):
        x = min( self.trackWidth - self.pixelsPerTick[self.curBeats], max( Config.TRACK_SPACING_DIV2, event.x ) )
        ticks = self.pixelsToTicks( self.curBeats, x )
        print "set playhead to %d ticks" % (ticks)
        self.doneCurrentAction()

    def updatePaste( self, tick, track ):
        if self.interfaceMode == INTERFACEMODE.PASTE_TRACKS: tick = 0
        if self.pasteTick == tick and self.pasteTrack == track: return
        if self.noteDB.getPage(self.curPage).ticks < tick < 0 \
           or track > self.drumIndex \
           or ( track == self.drumIndex and not self.clipboardDrumTrack ):
            if self.pasteRect:
                self.invalidate_rect( self.pasteRect[0][0], self.pasteRect[0][1], self.pasteRect[1][0], self.pasteRect[1][1], self.curPage, False )
                self.pasteTick = self.pasteTrack = -1
                self.pasteRect = False
            return
        if self.pasteRect:
            self.invalidate_rect( self.pasteRect[0][0], self.pasteRect[0][1], self.pasteRect[1][0], self.pasteRect[1][1], self.curPage, False )
        if self.clipboardDrumTrack:
            bottom = self.drumIndex
        else:
            bottom = self.drumIndex - 1
            for t in range(self.drumIndex-1,self.clipboardTrackTop-1,-1):
                if self.clipboardArea["tracks"][t]: break
                bottom -= 1
        end = -tick + min( self.noteDB.getPage(self.curPage).ticks, tick + self.clipboardArea["limit"][1]-self.clipboardArea["limit"][0] )
        self.pasteTick = tick
        self.pasteTrack = track
        self.pasteRect = [ [ self.ticksToPixels( self.curBeats, tick ), \
                             self.trackLimits[track][0] ], \
                           [ self.ticksToPixels( self.curBeats, end), \
                             self.trackLimits[bottom][1] ] ]
        self.invalidate_rect( self.pasteRect[0][0], self.pasteRect[0][1], self.pasteRect[1][0], self.pasteRect[1][1], self.curPage, False )

    def doPaste( self ):
        if self.pasteTrack == -1:
            self.doneCurrentAction()
            return

        trackMap = {}
        for t in range(self.pasteTrack,self.drumIndex):
            ind = t+self.clipboardTrackTop-self.pasteTrack
            if ind >= self.drumIndex: break
            if not self.clipboardArea["tracks"][ind]:
                continue
            trackMap[t] = ind
        if self.clipboardDrumTrack:
            trackMap[self.drumIndex] = self.drumIndex
        new = self.owner.pasteClipboard( self.pasteTick - self.clipboardArea["limit"][0], trackMap )
        if self.interfaceMode == INTERFACEMODE.PASTE_NOTES and self.curPage in new:
            noteDic = {}
            for t in range(Config.NUMBER_OF_TRACKS):
                if len(new[self.curPage][t]):
                    noteDic[t] = [ self.noteDB.getNote( self.curPage, t, n, self ) for n in new[self.curPage][t] ]
            self.selectNotes(noteDic)
        elif self.interfaceMode == INTERFACEMODE.PASTE_TRACKS:
            for t in range(self.drumIndex):
                ind = t + self.clipboardTrackTop - self.pasteTrack
                if ind >= self.drumIndex or ind < 0: self.owner.setTrack( t, False )
                else: self.owner.setTrack( t, self.clipboardArea["tracks"][ind] )
            self.owner.setTrack( self.drumIndex, self.clipboardDrumTrack )

        self.doneCurrentAction()

    def donePaste( self ):
        if self.pasteRect:
            self.invalidate_rect( self.pasteRect[0][0], self.pasteRect[0][1], self.pasteRect[1][0], self.pasteRect[1][1], self.curPage, False )
            self.pasteTick = self.pasteTrack = -1
            self.pasteRect = False
        self.setInterfaceMode("tool")
        # make a fake event for updateTooltip
        event = gtk.gdk.Event(gtk.gdk.MOTION_NOTIFY)
        x, y, state = self.window.get_pointer()
        event.x = float(x)
        event.y = float(y)
        event.state = state
        self.updateTooltip( event )

    def updateTooltip( self, event ):

        # check clicked the playhead
        if event.x >= self.playheadX and event.x <= self.playheadX + Config.PLAYHEAD_SIZE:
            self.setCursor("drag-playhead")
            return

        if event.x < Config.TRACK_SPACING_DIV2 or event.x > self.trackWidth + Config.TRACK_SPACING_DIV2:
            self.setCursor("default")
            return

        for i in range(Config.NUMBER_OF_TRACKS):
            if self.trackLimits[i][0] > event.y: break
            if self.trackLimits[i][1] < event.y: continue

            notes = self.noteDB.getNotesByTrack( self.curPage, i, self )
            handled = 0
            for n in range(len(notes)):
                handled = notes[n].updateTooltip( self, event )
                if handled == 0:   continue
                elif handled == 1: return   # event was handled
                else:              break

            # note wasn't handled, could potentially draw a note
            if self.interfaceMode == INTERFACEMODE.DRAW:
                if handled == -2: # event X overlapped with a note
                    self.setCursor("default")
                    return

                self.setCursor("pencil")
                return

            break

        self.setCursor("default")

    def setCursor( self, cursor ):
        self.window.set_cursor(self.cursor[cursor])

    #=======================================================
    #  Drawing

    def draw( self, buf, noescape = True, timeout = 0 ):
        if not self.screenBufDirty[buf]: return True

        TP.ProfileBegin( "TrackInterface::draw" )

        startX = self.screenBufDirtyRect[buf].x
        startY = self.screenBufDirtyRect[buf].y
        stopX = self.screenBufDirtyRect[buf].x + self.screenBufDirtyRect[buf].width
        stopY = self.screenBufDirtyRect[buf].y + self.screenBufDirtyRect[buf].height

        beatStart = Config.TRACK_SPACING_DIV2
        beats = self.screenBufBeats[buf]

        pixmap = self.screenBuf[buf]

        resume = self.screenBufResume[buf]

        self.gc.set_clip_rectangle( self.screenBufDirtyRect[buf] )

        self.gc.set_line_attributes( Config.BEAT_LINE_SIZE, gtk.gdk.LINE_ON_OFF_DASH, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER )
        # regular tracks
        for i in range( resume[0], self.drumIndex ):
            if resume[1] == 0:
                if startY > self.trackLimits[i][1]: continue
                if stopY < self.trackLimits[i][0]: break

                # draw background
                if self.owner.getTrackSelected( i ):
                    pixmap.draw_drawable( self.gc, self.image["trackBGSelected"], 0, 0, 0, self.trackLimits[i][0], self.trackFullWidth, self.trackFullHeight )
                else:
                    pixmap.draw_drawable( self.gc, self.image["trackBG"], 0, 0, 0, self.trackLimits[i][0], self.trackFullWidth, self.trackFullHeight )

                # draw beat lines
                self.gc.foreground = self.beatColor
                for j in range(1,self.screenBufBeats[buf]):
                    x = beatStart + self.beatSpacing[beats][j]
                    pixmap.draw_line( self.gc, x, self.trackRect[i].y, x, self.trackRect[i].y+self.trackRect[i].height )

                resume[1] = 1 # background drawn

            # draw notes
            TP.ProfileBegin("TI::draw notes")
            notes = self.noteDB.getNotesByTrack( self.screenBufPage[buf], i, self )
            for n in range( resume[2], len(notes) ):
                # check escape
                if not noescape and time.time() > timeout:
                    resume[0] = i
                    resume[2] = n
                    TP.ProfilePause( "TrackInterface::draw" )
                    return False

                if not notes[n].draw( pixmap, self.gc, startX, stopX ): break
            TP.ProfileEnd("TI::draw notes")

            # finished a track, reset the resume values for the next one
            resume[1] = 0
            resume[2] = 0

        # drum track
        if stopY > self.trackLimits[self.drumIndex][0]:

            if resume[1] == 0:
                # draw background
                if self.owner.getTrackSelected( self.drumIndex ):
                    pixmap.draw_drawable( self.gc, self.image["trackBGDrumSelected"], 0, 0, 0, self.trackLimits[self.drumIndex][0], self.trackFullWidth, self.trackFullHeightDrum )
                else:
                    pixmap.draw_drawable( self.gc, self.image["trackBGDrum"], 0, 0, 0, self.trackLimits[self.drumIndex][0], self.trackFullWidth, self.trackFullHeightDrum )

                # draw beat lines
                self.gc.foreground = self.beatColor
                for j in range(1,self.screenBufBeats[buf]):
                    x = beatStart + self.beatSpacing[beats][j]
                    pixmap.draw_line( self.gc, x, self.trackRect[self.drumIndex].y, x, self.trackRect[self.drumIndex].y+self.trackRect[self.drumIndex].height )

                resume[1] = 1 # background drawn

            # draw notes
            notes = self.noteDB.getNotesByTrack( self.screenBufPage[buf], self.drumIndex, self )
            for n in range( resume[2], len(notes) ):
                # check escape
                if not noescape and time.time() > timeout:
                    resume[0] = i
                    resume[2] = n
                    TP.ProfilePause( "TrackInterface::draw" )
                    return False
                if not notes[n].draw( pixmap, self.gc, startX, stopX ): break

        self.screenBufDirty[buf] = False

        TP.ProfileEnd( "TrackInterface::draw" )

        return True

    def expose( self, DA, event ):

        if self.screenBufDirty[self.curScreen]:
            self.draw( self.curScreen )

        TP.ProfileBegin( "TrackInterface::expose" )

        startX = event.area.x
        startY = event.area.y
        stopX = event.area.x + event.area.width
        stopY = event.area.y + event.area.height

        #print "%d %d %d %d" % (startX,startY,stopX,stopY)

        self.gc.set_clip_rectangle( event.area )

        # draw base
        DA.window.draw_drawable( self.gc, self.screenBuf[self.curScreen], startX, startY, startX, startY, event.area.width, event.area.height )

        # draw playhead
        self.gc.set_line_attributes( Config.PLAYHEAD_SIZE, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER )
        self.gc.foreground = self.playheadColor
        DA.window.draw_line( self.gc, self.playheadX, startY, self.playheadX, stopY )

        if self.marqueeLoc:                 # draw the selection rect
            self.gc.set_line_attributes( Config.MARQUEE_SIZE, gtk.gdk.LINE_ON_OFF_DASH, gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_MITER )
            self.gc.foreground = self.marqueeColor
            DA.window.draw_rectangle( self.gc, False, self.marqueeRect[0][0], self.marqueeRect[0][1], self.marqueeRect[1][0], self.marqueeRect[1][1] )

        if self.pasteRect:                  # draw the paste highlight
            self.gc.set_function( gtk.gdk.INVERT )
            for t in range(self.pasteTrack,self.drumIndex):
                ind = t+self.clipboardTrackTop-self.pasteTrack
                if ind >= self.drumIndex: break
                if not self.clipboardArea["tracks"][ind]:
                    continue
                DA.window.draw_rectangle( self.gc, True, self.pasteRect[0][0], self.trackLimits[t][0] + Config.TRACK_SPACING_DIV2, self.pasteRect[1][0], self.trackHeight )
            if self.clipboardDrumTrack:
                DA.window.draw_rectangle( self.gc, True, self.pasteRect[0][0], self.trackLimits[self.drumIndex][0] + Config.TRACK_SPACING_DIV2, self.pasteRect[1][0], self.trackHeightDrum )
            self.gc.set_function( gtk.gdk.COPY )

        self.drawingAreaDirty = False

        TP.ProfileEnd( "TrackInterface::expose" )

    def invalidate_rect( self, x, y, width, height, page = -1, base = True ):
        #print "%d %d %d %d Page %d CurPage %d" % (x,y,width,height,page,self.curPage)
        self.dirtyRectToAdd.x = x
        self.dirtyRectToAdd.y = y
        self.dirtyRectToAdd.width = width
        self.dirtyRectToAdd.height = height

        #print "dirty %d %d %d %d %d %d" % (x, y, width, height, x+width, y+height)
        if page == self.curPage or page == -1:
            if base: # the base image has been dirtied
                if not self.screenBufDirty[self.curScreen]:
                    self.screenBufDirtyRect[self.curScreen].x = x
                    self.screenBufDirtyRect[self.curScreen].y = y
                    self.screenBufDirtyRect[self.curScreen].width = width
                    self.screenBufDirtyRect[self.curScreen].height = height
                else:
                    self.screenBufDirtyRect[self.curScreen] = self.screenBufDirtyRect[self.curScreen].union( self.dirtyRectToAdd )
                self.screenBufResume[self.curScreen] = [0,0,0]
                self.screenBufDirty[self.curScreen] = True
            if self.drawingArea.window != None:
                self.drawingArea.window.invalidate_rect( self.dirtyRectToAdd, True )
            self.drawingAreaDirty = True

        if page == self.screenBufPage[self.preScreen] or page == -1:
            if not self.screenBufDirty[self.preScreen]:
                self.screenBufDirtyRect[self.preScreen].x = x
                self.screenBufDirtyRect[self.preScreen].y = y
                self.screenBufDirtyRect[self.preScreen].width = width
                self.screenBufDirtyRect[self.preScreen].height = height
            else:
                self.screenBufDirtyRect[self.preScreen] = self.screenBufDirtyRect[self.preScreen].union( self.dirtyRectToAdd )
            self.screenBufResume[self.preScreen] = [0,0,0]
            self.screenBufDirty[self.preScreen] = True

        #self.queue_draw()

    def getTrackOrigin( self, track ):
        return ( self.trackRect[track].x, self.trackRect[track].y )

    def ticksToPixels( self, beats, ticks ):
        return int(round( ticks * self.pixelsPerTick[beats] ))
    def pixelsToTicks( self, beats, pixels ):
        return int(round( pixels * self.ticksPerPixel[beats] ))
    def ticksToPixelsFloor( self, beats, ticks ):
        return int( ticks * self.pixelsPerTick[beats] )
    def pixelsToTicksFloor( self, beats, pixels ):
        return int( pixels * self.ticksPerPixel[beats] )
    def pitchToPixels( self, pitch ):
        return int(round( ( Config.MAXIMUM_PITCH - pitch ) * self.pixelsPerPitch ))
    def pixelsToPitch( self, pixels ):
        return int(round(-pixels*self.pitchPerPixel))
    def pitchToPixelsFloor( self, pitch ):
        return int(( Config.MAXIMUM_PITCH - pitch ) * self.pixelsPerPitch )
    def pixelsToPitchFloor( self, pixels ):
        return int(-pixels*self.pitchPerPixel)
    def pitchToPixelsDrum( self, pitch ):
        return int(round( ( Config.MAXIMUM_PITCH_DRUM - pitch ) * self.pixelsPerPitchDrum ))
    def pixelsToPitchDrum( self, pixels ):
        return int(round(-pixels*self.pitchPerPixelDrum))
    def pitchToPixelsDrumFloor( self, pitch ):
        return int( ( Config.MAXIMUM_PITCH_DRUM - pitch ) * self.pixelsPerPitchDrum )
    def pixelsToPitchDrumFloor( self, pixels ):
        return int(-pixels*self.pitchPerPixelDrum)
