Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoramartin <olpc@localhost.localdomain>2007-02-09 01:58:04 (GMT)
committer amartin <olpc@localhost.localdomain>2007-02-09 01:58:04 (GMT)
commit34df2f3075dad2b2a91d3ad2f7d5201d73ad07b9 (patch)
tree5c218ac84f2dcd83759edef8af313075e6313524
parentdbf0e2d924840be32f60611ae7ae8e09111a8740 (diff)
track delete/duplicate, note delete/duplicate, page predrawing, misc bug fixes
-rw-r--r--Config.py8
-rw-r--r--Edit/HitInterface.py14
-rw-r--r--Edit/MainWindow.py351
-rw-r--r--Edit/NoteInterface.py37
-rw-r--r--Edit/TrackInterface.py294
-rw-r--r--Edit/TuneInterface.py89
-rw-r--r--Util/NoteDB.py160
-rw-r--r--scripts/clean_trailing_whitespace.py2
8 files changed, 768 insertions, 187 deletions
diff --git a/Config.py b/Config.py
index c35c59f..4427eda 100644
--- a/Config.py
+++ b/Config.py
@@ -472,12 +472,12 @@ PLAYER_TEMPO_UPPER = 200
DEFAULT_VOLUME = 80
#NUMERICAL CONSTANTS
-NUMBER_OF_POSSIBLE_PITCHES = 25.0
-MINIMUM_PITCH = 24.0
+NUMBER_OF_POSSIBLE_PITCHES = 25
+MINIMUM_PITCH = 24
MAXIMUM_PITCH = MINIMUM_PITCH + NUMBER_OF_POSSIBLE_PITCHES - 1
-NUMBER_OF_POSSIBLE_PITCHES_DRUM = 13.0
+NUMBER_OF_POSSIBLE_PITCHES_DRUM = 13
PITCH_STEP_DRUM = 2
-MINIMUM_PITCH_DRUM = 24.0
+MINIMUM_PITCH_DRUM = 24
MAXIMUM_PITCH_DRUM = MINIMUM_PITCH_DRUM + PITCH_STEP_DRUM*(NUMBER_OF_POSSIBLE_PITCHES_DRUM - 1)
MINIMUM_NOTE_DURATION = 1 # ticks
MS_PER_MINUTE = 60000.0
diff --git a/Edit/HitInterface.py b/Edit/HitInterface.py
index 1301a33..f8cf691 100644
--- a/Edit/HitInterface.py
+++ b/Edit/HitInterface.py
@@ -18,10 +18,14 @@ class HitInterface( NoteInterface ):
self.updateTransform()
def updateTransform( self ):
- if self.note.page == self.owner.curPage and not self.firstTransform:
- oldX = self.imgX
- oldY = self.imgY
- oldEndX = self.imgX + self.imgWidth
+ if self.note.page in self.owner.getActivePages():
+ if not self.firstTransform:
+ oldX = self.imgX
+ oldY = self.imgY
+ oldEndX = self.imgX + self.imgWidth
+ dirty = True
+ else:
+ dirty = False
if self.note.cs.onset != self.oldOnset:
self.x = self.owner.ticksToPixels( self.noteDB.getPage(self.note.page).beats, self.note.cs.onset )
@@ -33,7 +37,7 @@ class HitInterface( NoteInterface ):
self.imgY = self.y - Config.NOTE_IMAGE_PADDING
self.oldPitch = self.note.cs.pitch
- if self.note.page == self.owner.curPage:
+ if dirty:
if self.firstTransform:
self.owner.invalidate_rect( self.imgX, self.imgY, self.imgWidth, self.imgHeight, self.note.page )
self.firstTransform = False
diff --git a/Edit/MainWindow.py b/Edit/MainWindow.py
index 2a8465b..b3e9512 100644
--- a/Edit/MainWindow.py
+++ b/Edit/MainWindow.py
@@ -207,17 +207,17 @@ class MainWindow( gtk.EventBox ):
self.GUI["2toolPanel"].set_size_request( -1, 75 )
# + + tool box
self.GUI["2toolBox"] = formatRoundBox( RoundHBox(), "#6C9790" )
- self.GUI["2toolBox"].set_size_request( 146, -1 )
+ self.GUI["2toolBox"].set_size_request( 154, -1 )
self.GUI["2toolPointerButton"] = ImageRadioButton( None, Config.IMAGE_ROOT+"pointer.png", Config.IMAGE_ROOT+"pointerDown.png", backgroundFill = "#6C9790" )
- self.GUI["2toolPointerButton"].connect( "clicked", self.handleToolClick , "Default" )
+ self.GUI["2toolPointerButton"].connect( "clicked", self.handleToolClick , "default" )
self.GUI["2toolBox"].pack_start( self.GUI["2toolPointerButton"] )
self.GUI["2toolPencilButton"] = ImageRadioButton( self.GUI["2toolPointerButton"], Config.IMAGE_ROOT+"pencil.png", Config.IMAGE_ROOT+"pencilDown.png", backgroundFill = "#6C9790" )
- self.GUI["2toolPencilButton"].connect( "clicked", self.handleToolClick , "Draw" )
+ self.GUI["2toolPencilButton"].connect( "clicked", self.handleToolClick , "draw" )
self.GUI["2toolBox"].pack_start( self.GUI["2toolPencilButton"] )
self.GUI["2toolPanel"].pack_start( self.GUI["2toolBox"], False, False )
self.GUI["2rightPanel"].pack_start( self.GUI["2toolPanel"], False )
# + + context box (for context sensitive buttons, nothing to do with CAIRO)
- contextWidth = 592
+ contextWidth = 674
self.GUI["2contextBox"] = formatRoundBox( RoundFixed(), "#6C9790" )
self.GUI["2contextBox"].set_size_request( contextWidth, -1 )
self.GUI["2contextPrevButton"] = gtk.Button("<")
@@ -229,48 +229,92 @@ class MainWindow( gtk.EventBox ):
# + + + page box
self.GUI["2pageBox"] = gtk.HBox()
self.GUI["2pageBox"].set_size_request( contextWidth-50, -1 )
+ self.GUI["2pageGenerateButton"] = gtk.Button("Gen")
+ self.GUI["2pageGenerateButton"].connect( "clicked", lambda a1:self.pageGenerate() )
+ self.GUI["2pageBox"].pack_start( self.GUI["2pageGenerateButton"] )
+ self.GUI["2pagePropertiesButton"] = gtk.Button("Prop")
+ self.GUI["2pagePropertiesButton"].connect( "clicked", lambda a1:self.pageProperties() )
+ self.GUI["2pageBox"].pack_start( self.GUI["2pagePropertiesButton"] )
self.GUI["2pageDeleteButton"] = gtk.Button("Delete")
- self.GUI["2pageDeleteButton"].connect( "clicked", lambda a1:self.removePages() )
+ self.GUI["2pageDeleteButton"].connect( "clicked", lambda a1:self.pageDelete() )
self.GUI["2pageBox"].pack_start( self.GUI["2pageDeleteButton"] )
- self.GUI["2pageNewButton"] = gtk.Button("New")
- self.GUI["2pageNewButton"].connect( "clicked", lambda a1:self.addPage() )
- self.GUI["2pageBox"].pack_start( self.GUI["2pageNewButton"] )
self.GUI["2pageDuplicateButton"] = gtk.Button("Duplicate")
- self.GUI["2pageDuplicateButton"].connect( "clicked", lambda a1:self.duplicatePages() )
+ self.GUI["2pageDuplicateButton"].connect( "clicked", lambda a1:self.pageDuplicate() )
self.GUI["2pageBox"].pack_start( self.GUI["2pageDuplicateButton"] )
+ self.GUI["2pageNewButton"] = gtk.Button("New")
+ self.GUI["2pageNewButton"].connect( "clicked", lambda a1:self.pageAdd() )
+ self.GUI["2pageBox"].pack_start( self.GUI["2pageNewButton"] )
+ self.GUI["2pageBeatsButton"] = gtk.Button("Beats")
+ self.GUI["2pageBeatsButton"].connect( "clicked", lambda a1:self.pageBeats() )
+ self.GUI["2pageBox"].pack_start( self.GUI["2pageBeatsButton"] )
self.GUI["2contextBox"].put( self.GUI["2pageBox"], 25, 0 )
# + + + track box
self.GUI["2trackBox"] = gtk.HBox()
self.GUI["2trackBox"].set_size_request( contextWidth-50, -1 )
+ self.GUI["2trackGenerateButton"] = gtk.Button("tGen")
+ self.GUI["2trackGenerateButton"].connect( "clicked", lambda a1:self.trackGenerate() )
+ self.GUI["2trackBox"].pack_start( self.GUI["2trackGenerateButton"] )
+ self.GUI["2trackPropertiesButton"] = gtk.Button("tProp")
+ self.GUI["2trackPropertiesButton"].connect( "clicked", lambda a1:self.trackProperties() )
+ self.GUI["2trackBox"].pack_start( self.GUI["2trackPropertiesButton"] )
self.GUI["2trackDeleteButton"] = gtk.Button("tDelete")
- self.GUI["2trackDeleteButton"].connect( "clicked", lambda a1:self.removePages() )
+ self.GUI["2trackDeleteButton"].connect( "clicked", lambda a1:self.trackDelete() )
self.GUI["2trackBox"].pack_start( self.GUI["2trackDeleteButton"] )
- self.GUI["2trackNewButton"] = gtk.Button("tNew")
- self.GUI["2trackNewButton"].connect( "clicked", lambda a1:self.addPage() )
- self.GUI["2trackBox"].pack_start( self.GUI["2trackNewButton"] )
- self.GUI["2trackDuplicateButton"] = gtk.Button("tDuplicate")
- self.GUI["2trackDuplicateButton"].connect( "clicked", lambda a1:self.duplicatePages() )
+ self.GUI["2trackDuplicateButton"] = gtk.ToggleButton("tDuplicate")
+ self.GUI["2trackDuplicateButton"].connect( "toggled", lambda a1:self.trackDuplicate() )
self.GUI["2trackBox"].pack_start( self.GUI["2trackDuplicateButton"] )
self.GUI["2contextBox"].put( self.GUI["2trackBox"], 25, 0 )
# + + + note box
self.GUI["2noteBox"] = gtk.HBox()
self.GUI["2noteBox"].set_size_request( contextWidth-50, -1 )
+ self.GUI["2noteGenerateButton"] = gtk.Button("nGen")
+ self.GUI["2noteGenerateButton"].connect( "clicked", lambda a1:self.noteGenerate() )
+ self.GUI["2noteBox"].pack_start( self.GUI["2noteGenerateButton"] )
+ self.GUI["2notePropertiesButton"] = gtk.Button("nProp")
+ self.GUI["2notePropertiesButton"].connect( "clicked", lambda a1:self.noteProperties() )
+ self.GUI["2noteBox"].pack_start( self.GUI["2notePropertiesButton"] )
self.GUI["2noteDeleteButton"] = gtk.Button("nDelete")
- self.GUI["2noteDeleteButton"].connect( "clicked", lambda a1:self.removePages() )
+ self.GUI["2noteDeleteButton"].connect( "clicked", lambda a1:self.noteDelete() )
self.GUI["2noteBox"].pack_start( self.GUI["2noteDeleteButton"] )
- self.GUI["2noteNewButton"] = gtk.Button("nNew")
- self.GUI["2noteNewButton"].connect( "clicked", lambda a1:self.addPage() )
- self.GUI["2noteBox"].pack_start( self.GUI["2noteNewButton"] )
- self.GUI["2noteDuplicateButton"] = gtk.Button("nDuplicate")
- self.GUI["2noteDuplicateButton"].connect( "clicked", lambda a1:self.duplicatePages() )
+ self.GUI["2noteDuplicateButton"] = gtk.ToggleButton("nDuplicate")
+ self.GUI["2noteDuplicateButton"].connect( "toggled", self.noteDuplicateWidget )
self.GUI["2noteBox"].pack_start( self.GUI["2noteDuplicateButton"] )
+ self.GUI["2noteOnsetBox"] = gtk.HBox()
+ self.GUI["2noteOnsetMinusButton"] = gtk.Button("<")
+ self.GUI["2noteOnsetMinusButton"].connect( "clicked", lambda a1:self.noteOnset(-1) )
+ self.GUI["2noteOnsetBox"].pack_start( self.GUI["2noteOnsetMinusButton"] )
+ self.GUI["2noteOnsetPlusButton"] = gtk.Button(">")
+ self.GUI["2noteOnsetPlusButton"].connect( "clicked", lambda a1:self.noteOnset(1) )
+ self.GUI["2noteOnsetBox"].pack_start( self.GUI["2noteOnsetPlusButton"] )
+ self.GUI["2noteBox"].pack_start( self.GUI["2noteOnsetBox"] )
+ self.GUI["2notePitchBox"] = gtk.VBox()
+ self.GUI["2notePitchPlusButton"] = gtk.Button("^")
+ self.GUI["2notePitchPlusButton"].connect( "clicked", lambda a1:self.notePitch(1) )
+ self.GUI["2notePitchBox"].pack_start( self.GUI["2notePitchPlusButton"] )
+ self.GUI["2notePitchMinusButton"] = gtk.Button("v")
+ self.GUI["2notePitchMinusButton"].connect( "clicked", lambda a1:self.notePitch(-1) )
+ self.GUI["2notePitchBox"].pack_start( self.GUI["2notePitchMinusButton"] )
+ self.GUI["2noteBox"].pack_start( self.GUI["2notePitchBox"] )
+ self.GUI["2noteDurationBox"] = gtk.HBox()
+ self.GUI["2noteDurationMinusButton"] = gtk.Button("<")
+ self.GUI["2noteDurationMinusButton"].connect( "clicked", lambda a1:self.noteDuration(-1) )
+ self.GUI["2noteDurationBox"].pack_start( self.GUI["2noteDurationMinusButton"] )
+ self.GUI["2noteDurationPlusButton"] = gtk.Button(">")
+ self.GUI["2noteDurationPlusButton"].connect( "clicked", lambda a1:self.noteDuration(1) )
+ self.GUI["2noteDurationBox"].pack_start( self.GUI["2noteDurationPlusButton"] )
+ self.GUI["2noteBox"].pack_start( self.GUI["2noteDurationBox"] )
+ self.GUI["2noteVolumeBox"] = gtk.VBox()
+ self.GUI["2noteVolumePlusButton"] = gtk.Button("^")
+ self.GUI["2noteVolumePlusButton"].connect( "clicked", lambda a1:self.noteVolume(1) )
+ self.GUI["2noteVolumeBox"].pack_start( self.GUI["2noteVolumePlusButton"] )
+ self.GUI["2noteVolumeMinusButton"] = gtk.Button("v")
+ self.GUI["2noteVolumeMinusButton"].connect( "clicked", lambda a1:self.noteVolume(-1) )
+ self.GUI["2noteVolumeBox"].pack_start( self.GUI["2noteVolumeMinusButton"] )
+ self.GUI["2noteBox"].pack_start( self.GUI["2noteVolumeBox"] )
self.GUI["2contextBox"].put( self.GUI["2noteBox"], 25, 0 )
self.GUI["2toolPanel"].pack_start( self.GUI["2contextBox"], False )
# + + transport box
self.GUI["2transportBox"] = formatRoundBox( RoundHBox(), "#6C9790" )
- self.GUI["2generateButton"] = gtk.Button("G")
- self.GUI["2generateButton"].connect( "clicked", self.handleGenerate, None )
- self.GUI["2transportBox"].pack_start( self.GUI["2generateButton"] )
self.GUI["2recordButton"] = gtk.ToggleButton("R")
self.GUI["2transportBox"].pack_start( self.GUI["2recordButton"] )
self.GUI["2playButton"] = gtk.ToggleButton("P")
@@ -311,6 +355,10 @@ class MainWindow( gtk.EventBox ):
self.currentpageId = 0
self.playingTuneIdx = 0
+ # timers
+ self.predrawTimeout = False
+ self.playbackTimeout = False
+
# FPS stuff
self.fpsTotalTime = 0
self.fpsFrameCount = 0
@@ -323,7 +371,7 @@ class MainWindow( gtk.EventBox ):
init_data() #above
init_GUI() #above
-
+
self.csnd.setMasterVolume( self.getVolume() )
for tid in range(Config.NUMBER_OF_TRACKS):
@@ -376,7 +424,10 @@ class MainWindow( gtk.EventBox ):
notes += self.noteDB.getCSNotesByTrack( page, track )
self.playing = True
- self.playbackTimeout = gobject.timeout_add( 100, self.onTimeout )
+ if self.predrawTimeout:
+ gobject.source_remove( self.predrawTimeout )
+ self.predrawTimeout = False
+ self.playbackTimeout = gobject.timeout_add( 50, self.onTimeout )
if len(self.pages_playing) > 1:
self.displayPage( self.pages_playing[0], self.pages_playing[1] )
@@ -407,6 +458,7 @@ class MainWindow( gtk.EventBox ):
else: #stop
gobject.source_remove( self.playbackTimeout )
+ self.playbackTimeout = False
if False:
#This is causing csound to stop working...
# reimplement this with real CSoundNotes and it should be ok.
@@ -426,10 +478,22 @@ class MainWindow( gtk.EventBox ):
curtick = self.csnd.loopGetTick()
curIdx = curtick / ( 4 * Config.TICKS_PER_BEAT) #TODO handle each pages_playing length
- if curIdx + 1 < len(self.pages_playing): predraw = self.pages_playing[curIdx+1]
- else: predraw = self.pages_playing[0]
- self.displayPage( self.pages_playing[curIdx], predraw )
+ # TODO update playhead
+
+ if self.pages_playing[curIdx] != self.displayedPage:
+ if curIdx + 1 < len(self.pages_playing): predraw = self.pages_playing[curIdx+1]
+ else: predraw = self.pages_playing[0]
+ self.displayPage( self.pages_playing[curIdx], predraw )
+ else:
+ self.trackInterface.predrawPage( time.time() + 0.020 ) # 20 ms time limit
+
+ return True
+
+ def onPredrawTimeout( self ):
+ if self.trackInterface.predrawPage( time.time() + 0.020 ): # 20 ms time limit
+ self.predrawTimeout = False
+ return False
return True
def onMuteTrack( self, widget, trackId ):
@@ -478,6 +542,10 @@ class MainWindow( gtk.EventBox ):
def handleToolClick( self, widget, mode ):
if widget.get_active(): self.trackInterface.setInterfaceMode( mode )
+ def getTool( self ):
+ if self.GUI["2toolPointerButton"].get_active(): return "default"
+ else: return "draw"
+
def onKeyboardButton( self, widget, data ):
self.kb_active = widget.get_active()
@@ -488,11 +556,6 @@ class MainWindow( gtk.EventBox ):
#-----------------------------------
# generation functions
#-----------------------------------
- def handleGenerate( self, widget, data ):
- #if widget.get_active():
- self.generationParametersWindow.show_all()
- #else:
- # self.handleCloseGeneratonParametersWindow()
def handleCloseGenerationParametersWindow( self, widget = None, data = None ):
self.generationParametersWindow.hide_all()
@@ -507,11 +570,19 @@ class MainWindow( gtk.EventBox ):
for p in range(Config.NUMBER_OF_PAGES):
dict[t][p] = []
- if self.trackSelected == [ 0 for i in range(Config.NUMBER_OF_TRACKS) ]:
+ if self.generateMode == "note":
+ # unsupported!?
+ self.handleCloseGenerationParametersWindow( None, None )
+ return
+ elif self.generateMode == "track":
+ if self.trackSelected == [ 0 for i in range(Config.NUMBER_OF_TRACKS) ]:
+ newtracks = set(range(Config.NUMBER_OF_TRACKS))
+ else:
+ newtracks = set( [ i for i in range(Config.NUMBER_OF_TRACKS) if self.trackSelected[i] ] )
+ newpages = self.tuneInterface.getSelectedIds()
+ else: # page mode
newtracks = set(range(Config.NUMBER_OF_TRACKS))
- else:
- newtracks = set( [ i for i in range(Config.NUMBER_OF_TRACKS) if self.trackSelected[i] ] )
- newpages = self.tuneInterface.getSelectedIds()
+ newpages = self.tuneInterface.getSelectedIds()
algo(
params,
@@ -532,17 +603,21 @@ class MainWindow( gtk.EventBox ):
note.pageId = page
note.trackId = track
- # add the new notes
+ # prepare the new notes
newnotes = []
for tid in dict:
for pid in dict[tid]:
newnotes += dict[tid][pid]
# delete the notes and add the new
+ if self.generateMode == "note":
+ print "TODO generateMode == note"
+ else: # track or page mode
+ self.noteDB.deleteNotesByTrack( newpages, newtracks )
+
stream = []
for page in newpages:
for track in newtracks:
- self.noteDB.deleteNotesByTrack( page, track )
stream += [ page, track, len(dict[track][page]) ]
stream += dict[track][page]
stream += [-1]
@@ -557,14 +632,97 @@ class MainWindow( gtk.EventBox ):
self.recompose( variate, params)
#=======================================================
+ # Clipboard Functions
+
+ def getClipboardArea( self, page = -1 ):
+ if page == -1: page = self.displayedPage
+ ids = self.tuneInterface.getSelectedIds()
+ return self.noteDB.getClipboardArea( ids.index(page) )
+
+ def pasteClipboard( self, offset, trackMap ):
+ pages = self.tuneInterface.getSelectedIds()
+ return self.noteDB.pasteClipboard( pages, offset, trackMap )
+
+ def cleanupClipboard( self ):
+ if self.GUI["2noteDuplicateButton"].get_active():
+ self.GUI["2noteDuplicateButton"].set_active(False)
+ if self.GUI["2trackDuplicateButton"].get_active():
+ self.GUI["2trackDuplicateButton"].set_active(False)
+ self.trackInterface.donePaste()
+
+
+ #=======================================================
+ # Note Functions
+
+ def noteGenerate( self ):
+ self.generateMode = "note"
+ self.generationParametersWindow.show_all()
+ return
+
+ def noteProperties( self ):
+ # TODO
+ return
+
+ def noteDelete( self ):
+ ids = self.trackInterface.getSelectedNotes()
+ stream = []
+ for t in range(Config.NUMBER_OF_TRACKS):
+ N = len(ids[t])
+ if not N: continue
+ stream += [ self.displayedPage, t, N ] + ids[t]
+ if len(stream):
+ self.noteDB.deleteNotes( stream + [-1] )
+
+ def noteDuplicate( self ):
+ ids = self.trackInterface.getSelectedNotes()
+ stream = []
+ for t in range(Config.NUMBER_OF_TRACKS):
+ N = len(ids[t])
+ if not N: continue
+ stream += [ self.displayedPage, t, N ] + ids[t]
+ if len(stream):
+ if self.GUI["2trackDuplicateButton"].get_active():
+ self.GUI["2trackDuplicateButton"].set_active( False )
+ self.noteDB.notesToClipboard( stream + [-1] )
+ self.trackInterface.setInterfaceMode("paste_notes")
+ return True
+ return False
+
+ def noteDuplicateWidget( self, widget ):
+ if widget.get_active():
+ if self.noteDuplicate(): # duplicate succeeded
+ return
+ # cancel duplicate
+ self.trackInterface.setInterfaceMode("tool")
+ widget.set_active(False)
+
+ def noteOnset( self, step ):
+ # TODO
+ return
+
+ def notePitch( self, step ):
+ # TODO
+ return
+
+ def noteDuration( self, step ):
+ # TODO
+ return
+
+ def noteVolume( self, step ):
+ # TODO
+ return
+
+ #=======================================================
# Track Functions
def toggleTrack( self, trackN, exclusive ):
if exclusive:
for i in range(Config.NUMBER_OF_TRACKS):
- self.trackSelected[i] = False
+ if self.trackSelected[i]:
+ self.trackSelected[i] = False
+ self.trackInterface.trackToggled( i )
self.trackSelected[trackN] = True
- self.trackInterface.trackToggled() # invalidate whole page
+ self.trackInterface.trackToggled( trackN )
self.setContextState( CONTEXT.TRACK, True )
self.setContext( CONTEXT.TRACK )
else:
@@ -577,27 +735,60 @@ class MainWindow( gtk.EventBox ):
return
self.setContextState( CONTEXT.TRACK, False )
+ def setTrack( self, trackN, state ):
+ if self.trackSelected[trackN] != state:
+ self.trackSelected[trackN] = state
+ self.trackInterface.trackToggled( trackN )
+
+ def clearTracks( self ):
+ for i in range(Config.NUMBER_OF_TRACKS):
+ if self.trackSelected[i]:
+ self.trackSelected[i]= False
+ self.trackInterface.trackToggled( i )
+
+ self.setContextState( CONTEXT.TRACK, False )
+
def getTrackSelected( self, trackN ):
return self.trackSelected[trackN]
- #=======================================================
- # NoteDB notifications
+ def trackGenerate( self ):
+ self.generateMode = "track"
+ self.generationParametersWindow.show_all()
- def notifyPageAdd( self, id, at ):
- self.displayPage( id )
+ def trackProperties( self, trackIds = -1 ):
+ # TODO
+ return
- def notifyPageDelete( self, which, safe ):
- if self.displayedPage in which:
- self.displayPage( safe )
+ def trackDelete( self, pageIds = -1, trackIds = -1 ):
- def notifyPageDuplicate( self, new, at ):
- self.displayPage( new[self.displayedPage] )
+ if pageIds == -1: pageIds = self.tuneInterface.getSelectedIds()
+ if trackIds == -1: trackIds = [ i for i in range(Config.NUMBER_OF_TRACKS) if self.trackSelected[i] ]
- def notifyPageMove( self, which, low, high ):
- return
+ self.noteDB.deleteNotesByTrack( pageIds, trackIds )
+
+ def trackDuplicate( self, pageIds = -1, trackIds = -1 ):
+
+ if pageIds == -1: pageIds = self.tuneInterface.getSelectedIds()
+ if trackIds == -1: trackIds = [ i for i in range(Config.NUMBER_OF_TRACKS) if self.trackSelected[i] ]
+
+ if len(trackIds):
+ if self.GUI["2noteDuplicateButton"].get_active():
+ self.GUI["2noteDuplicateButton"].set_active( False )
+ self.noteDB.tracksToClipboard( pageIds, trackIds )
+ self.trackInterface.setInterfaceMode("paste_tracks")
+ return True
+ return False
+
+ def trackDuplicateWidget( self, widget ):
+ if widget.get_active():
+ if self.trackDuplicate(): # duplicate succeeded
+ return
+ # cancel duplicate
+ self.trackInterface.setInterfaceMode("tool")
+ widget.set_active(False)
#-----------------------------------
- # tune functions
+ # tune/page functions
#-----------------------------------
def scrollTune( self, scroll ):
@@ -614,25 +805,63 @@ class MainWindow( gtk.EventBox ):
self.trackInterface.displayPage( pageId, nextId )
- def addPage( self, after = -1, beats = False ):
+ def predrawPage( self, pageId ):
+ if self.playbackTimeout: return # we're playing, predrawing is already handled
+ if self.trackInterface.setPredrawPage( pageId ): # page needs to be drawn
+ if not self.predrawTimeout:
+ self.predrawTimeout = gobject.timeout_add( 50, self.onPredrawTimeout )
- if after == -1: after = self.tuneInterface.getLastSelected()
- if not beats: beats = self.noteDB.getPage( self.displayedPage ).beats
+ def pageGenerate( self ):
+ self.generateMode = "page"
+ self.generationParametersWindow.show_all()
- self.noteDB.addPage( beats, after )
+ def pageProperties( self, pageIds = -1 ):
+
+ if pageIds == -1: pageIds = self.tuneInterface.getSelectedIds()
+
+ # TODO show properties or something
+
+ def pageDelete( self, pageIds = -1 ):
+
+ if pageIds == -1: pageIds = self.tuneInterface.getSelectedIds()
+
+ self.noteDB.deletePages( pageIds )
- def duplicatePages( self, after = -1, pageIds = False ):
+ def pageDuplicate( self, after = -1, pageIds = False ):
if after == -1: after = self.tuneInterface.getLastSelected()
if not pageIds: pageIds = self.tuneInterface.getSelectedIds()
self.noteDB.duplicatePages( pageIds, after )
- def removePages( self, pageIds = -1 ):
+ def pageAdd( self, after = -1, beats = False ):
+
+ if after == -1: after = self.tuneInterface.getLastSelected()
+ if not beats: beats = self.noteDB.getPage( self.displayedPage ).beats
+
+ self.noteDB.addPage( beats, after )
+
+ def pageBeats( self, pageIds = -1 ):
if pageIds == -1: pageIds = self.tuneInterface.getSelectedIds()
- self.noteDB.deletePages( pageIds )
+ # TODO change the beats
+
+ #=======================================================
+ # NoteDB notifications
+
+ def notifyPageAdd( self, id, at ):
+ self.displayPage( id )
+
+ def notifyPageDelete( self, which, safe ):
+ if self.displayedPage in which:
+ self.displayPage( safe )
+
+ def notifyPageDuplicate( self, new, at ):
+ self.displayPage( new[self.displayedPage] )
+
+ def notifyPageMove( self, which, low, high ):
+ return
#-----------------------------------
# load and save functions
diff --git a/Edit/NoteInterface.py b/Edit/NoteInterface.py
index 9dc9e43..20886b5 100644
--- a/Edit/NoteInterface.py
+++ b/Edit/NoteInterface.py
@@ -40,7 +40,11 @@ class NoteInterface:
self.updateParameter( None, None )
def destroy( self ):
- self.owner.invalidate_rect( self.imgX, self.imgY, self.imgWidth, self.imgHeight, self.note.page, True )
+ if self.selected:
+ print "destroy", self.note.id
+ self.owner.deselectNotes( { self.note.track: [self] } )
+ else: # if we were deselected above the rect has already been invalidated
+ self.owner.invalidate_rect( self.imgX, self.imgY, self.imgWidth, self.imgHeight, self.note.page, True )
def updateParameter( self, parameter, value ):
self.end = self.note.cs.onset + self.note.cs.duration
@@ -70,10 +74,14 @@ class NoteInterface:
return self.note.cs.pitch
def updateTransform( self ):
- if self.note.page == self.owner.curPage and not self.firstTransform:
- oldX = self.imgX
- oldY = self.imgY
- oldEndX = self.imgX + self.imgWidth
+ if self.note.page in self.owner.getActivePages():
+ if not self.firstTransform:
+ oldX = self.imgX
+ oldY = self.imgY
+ oldEndX = self.imgX + self.imgWidth
+ dirty = True
+ else:
+ dirty = False
if self.note.cs.onset != self.oldOnset:
self.x = self.owner.ticksToPixels( self.noteDB.getPage( self.note.page).beats, self.note.cs.onset )
@@ -89,15 +97,16 @@ class NoteInterface:
self.imgY = self.y - Config.NOTE_IMAGE_PADDING
self.oldPitch = self.note.cs.pitch
- if self.firstTransform:
- self.owner.invalidate_rect( self.imgX, self.imgY, self.imgWidth, self.imgHeight, self.note.page, True )
- self.firstTransform = False
- else:
- x = min( self.imgX, oldX )
- y = min( self.imgY, oldY )
- endx = max( self.imgX + self.imgWidth, oldEndX )
- endy = max( self.imgY, oldY ) + self.imgHeight
- self.owner.invalidate_rect( x, y, endx-x, endy-y, self.note.page, True )
+ if dirty:
+ if self.firstTransform:
+ self.owner.invalidate_rect( self.imgX, self.imgY, self.imgWidth, self.imgHeight, self.note.page, True )
+ self.firstTransform = False
+ else:
+ x = min( self.imgX, oldX )
+ y = min( self.imgY, oldY )
+ endx = max( self.imgX + self.imgWidth, oldEndX )
+ endy = max( self.imgY, oldY ) + self.imgHeight
+ self.owner.invalidate_rect( x, y, endx-x, endy-y, self.note.page, True )
def updateDragLimits( self, dragLimits, leftBound, rightBound, widthBound, maxRightBound ):
left = leftBound - self.note.cs.onset
diff --git a/Edit/TrackInterface.py b/Edit/TrackInterface.py
index a3b3986..f33909d 100644
--- a/Edit/TrackInterface.py
+++ b/Edit/TrackInterface.py
@@ -3,6 +3,7 @@ pygtk.require( '2.0' )
import gtk
from math import floor
+import time
import Config
from Edit.NoteInterface import NoteInterface
@@ -22,7 +23,8 @@ class SELECTNOTES:
class INTERFACEMODE:
DEFAULT = 0
DRAW = 1
- PASTE = 2
+ PASTE_NOTES = 2
+ PASTE_TRACKS = 3
class TrackInterfaceParasite:
def __init__( self, noteDB, owner, note ):
@@ -66,15 +68,20 @@ class TrackInterface( gtk.EventBox ):
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.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.SB_V_DOUBLE_ARROW), \
- "drag-duration": gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW), \
- "drag-playhead": gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE), \
+ "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)
@@ -164,6 +171,16 @@ class TrackInterface( gtk.EventBox ):
self.curScreen = 0
self.preScreen = 1
+ #-- 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]
+
#=======================================================
# Module Interface
@@ -173,18 +190,32 @@ class TrackInterface( gtk.EventBox ):
else:
return ( self.image["note"], self.image["noteSelected"], self.drawingArea.get_colormap(), self.trackColors[track] )
- def predrawPage( self, page ):
+ 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, timeout ):
+ return self.draw( self.preScreen, False, timeout )
def displayPage( self, page, predraw = -1 ):
- if page == self.curPage: return
+ 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
@@ -204,21 +235,39 @@ class TrackInterface( gtk.EventBox ):
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()
+ self.clearSelectedNotes( oldPage )
+
+ if self.curAction == "paste":
+ self._updateClipboardArea()
def setPlayhead( self, ticks ):
self.invalidate_rect( self.playheadX-Config.PLAYHEAD_SIZE/2, 0, Config.PLAYHEAD_SIZE, self.height, self.curPage, False )
- self.playheadX = self.ticksToPixels( ticks, self.screenBufBeats[self.curScreen] ) + Config.TRACK_SPACING_DIV2
+ 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 )
def setInterfaceMode( self, mode ):
- if mode == "Draw":
+ self.doneCurrentAction()
+
+ if mode == "tool":
+ mode = self.owner.getTool()
+
+ if mode == "draw":
self.interfaceMode = INTERFACEMODE.DRAW
- elif mode == "Paste":
- self.interfaceMode = INTERFACEMODE.PASTE
+ 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
@@ -226,7 +275,7 @@ class TrackInterface( gtk.EventBox ):
self.alloc = allocation
width = allocation.width
height = allocation.height
-
+
self.drawingArea.set_size_request( width, height )
if self.window != None:
@@ -242,6 +291,13 @@ class TrackInterface( gtk.EventBox ):
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 )
@@ -289,8 +345,9 @@ class TrackInterface( gtk.EventBox ):
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 )
- else: self.owner.toggleTrack( i, True )
+ 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" )
@@ -317,7 +374,6 @@ class TrackInterface( gtk.EventBox ):
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)
@@ -326,7 +382,17 @@ class TrackInterface( gtk.EventBox ):
TP.ProfileEnd( "TI::handleMotion::Common" )
- if event.state & gtk.gdk.BUTTON1_MASK:
+ 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.pixelsToTicks( 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
@@ -361,9 +427,9 @@ class TrackInterface( gtk.EventBox ):
#=======================================================
# Actions
- def setCurrentAction( self, action, obj ):
+ def setCurrentAction( self, action, obj = None ):
if self.curAction:
- print "BackgroundView - Action already in progress!"
+ self.doneCurrentAction()
self.curAction = action
self.curActionObject = obj
@@ -372,19 +438,25 @@ class TrackInterface( gtk.EventBox ):
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 self.curAction == "note-drag-onset": self.doneNoteDrag()
- elif self.curAction == "note-drag-duration": self.doneNoteDrag()
- elif self.curAction == "note-drag-pitch": self.doneNoteDrag()
- elif self.curAction == "note-drag-pitch-drum": self.doneNoteDrag()
-
+ if not self.curAction: return
+ action = self.curAction
self.curAction = False
- self.curActionObject = False
+
+ if action == "note-drag-onset": self.doneNoteDrag()
+ elif action == "note-drag-duration": self.doneNoteDrag()
+ elif action == "note-drag-pitch": self.doneNoteDrag()
+ elif action == "note-drag-pitch-drum": self.doneNoteDrag()
+ elif action == "paste":
+ self.owner.cleanupClipboard()
def trackToggled( self, trackN = -1 ):
- if trackN == -1: self.invalidate_rect( 0, 0, self.width, self.height, self.curPage )
- else: self.invalidate_rect( 0, self.trackLimits[trackN][0], self.width, self.trackLimits[trackN][1]-self.trackLimits[trackN][0], self.curPage )
+ 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()
@@ -398,14 +470,15 @@ class TrackInterface( gtk.EventBox ):
return
self.owner.setContextState( CONTEXT.NOTE, False )
- def applyNoteSelection( self, mode, trackN, which ):
+ def applyNoteSelection( self, mode, trackN, which, page = -1 ):
+ if page == -1: page = self.curPage
if mode == SELECTNOTES.ALL:
- track = self.noteDB.getNotesByTrack( self.curPage, trackN, self )
+ 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.noteDB.getNotesByTrack( self.curPage, trackN, self )
+ track = self.noteDB.getNotesByTrack( page, trackN, self )
map( lambda note:note.setSelected( False ), track )
self.selectedNotes[trackN] = []
elif mode == SELECTNOTES.ADD:
@@ -425,7 +498,7 @@ class TrackInterface( gtk.EventBox ):
note.setSelected( True )
self.selectedNotes[trackN].append( note )
elif mode == SELECTNOTES.EXCLUSIVE:
- notes = self.noteDB.getNotesByTrack( self.curPage, trackN, self )
+ notes = self.noteDB.getNotesByTrack( page, trackN, self )
for n in range(len(notes)):
if notes[n] in which:
if notes[n].setSelected( True ):
@@ -434,46 +507,46 @@ class TrackInterface( gtk.EventBox ):
if notes[n].setSelected( False ):
self.selectedNotes[trackN].remove( notes[n] )
- def selectNotesByBar( self, trackN, start, stop ):
+ 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 )
- else: self.applyNoteSelection( SELECTNOTES.ADD, trackN, notes )
+ 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, [] )
+ if not Config.ModKeys.ctrlDown: self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
self.selectionChanged()
- def selectNotesByTrack( self, trackN ):
+ def selectNotesByTrack( self, trackN, page = -1 ):
if Config.ModKeys.ctrlDown:
- self.applyNoteSelection( SELECTNOTES.ALL, trackN, [] )
+ self.applyNoteSelection( SELECTNOTES.ALL, trackN, [], page )
else:
for i in range(Config.NUMBER_OF_TRACKS):
- if i == trackN: self.applyNoteSelection( SELECTNOTES.ALL, trackN, [] )
- else: self.applyNoteSelection( SELECTNOTES.NONE, i, [] )
+ if i == trackN: self.applyNoteSelection( SELECTNOTES.ALL, trackN, [], page )
+ else: self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
self.selectionChanged()
- def selectNotes( self, noteDic ):
- if Config.ModKeys.ctrlDown:
+ 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] )
+ 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] )
- else: self.applyNoteSelection( SELECTNOTES.NONE, i, [] )
+ 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 ):
+ def deselectNotes( self, noteDic, page = -1 ):
for i in noteDic:
- self.applyNoteSelection( SELECTNOTES.REMOVE, i, noteDic[i] )
+ self.applyNoteSelection( SELECTNOTES.REMOVE, i, noteDic[i], page )
self.selectionChanged()
- def clearSelectedNotes( self ):
+ def clearSelectedNotes( self, page = -1 ):
for i in range(Config.NUMBER_OF_TRACKS):
- self.applyNoteSelection( SELECTNOTES.NONE, i, [] )
+ self.applyNoteSelection( SELECTNOTES.NONE, i, [], page )
self.selectionChanged()
def updateDragLimits( self ):
@@ -616,6 +689,79 @@ class TrackInterface( gtk.EventBox ):
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
@@ -654,7 +800,9 @@ class TrackInterface( gtk.EventBox ):
#=======================================================
# Drawing
- def predraw( self, buf, noescape = True ):
+ def draw( self, buf, noescape = True, timeout = 0 ):
+ if not self.screenBufDirty[buf]: return True
+
TP.ProfileBegin( "TrackInterface::predraw" )
startX = self.screenBufDirtyRect[buf].x
@@ -675,7 +823,7 @@ class TrackInterface( gtk.EventBox ):
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[0] == 0:
+ if resume[1] == 0:
if startY > self.trackLimits[i][1]: continue
if stopY < self.trackLimits[i][0]: break
@@ -691,14 +839,16 @@ class TrackInterface( gtk.EventBox ):
x = beatStart + j*beatSpacing
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.curPage, i, self )
- for n in range( resume[1], len(notes) ):
+ notes = self.noteDB.getNotesByTrack( self.screenBufPage[buf], i, self )
+ for n in range( resume[2], len(notes) ):
# check escape
- if 0:
+ if not noescape and time.time() > timeout:
resume[0] = i
- resume[1] = n
+ resume[2] = n
TP.ProfilePause( "TrackInterface::predraw" )
return False
@@ -706,13 +856,13 @@ class TrackInterface( gtk.EventBox ):
TP.ProfileEnd("TI::draw notes")
# finished a track, reset the resume values for the next one
- resume[0] = 0
resume[1] = 0
+ resume[2] = 0
# drum track
if stopY > self.trackLimits[self.drumIndex][0]:
- if resume[0] == 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 )
@@ -725,13 +875,15 @@ class TrackInterface( gtk.EventBox ):
x = beatStart + j*beatSpacing
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.curPage, self.drumIndex, self )
- for n in range( resume[1], len(notes) ):
+ notes = self.noteDB.getNotesByTrack( self.screenBufPage[buf], self.drumIndex, self )
+ for n in range( resume[2], len(notes) ):
# check escape
- if 0:
+ if not noescape and time.time() > timeout:
resume[0] = i
- resume[1] = n
+ resume[2] = n
TP.ProfilePause( "TrackInterface::predraw" )
return False
if not notes[n].draw( pixmap, self.gc, startX, stopX ): break
@@ -745,7 +897,7 @@ class TrackInterface( gtk.EventBox ):
def expose( self, DA, event ):
if self.screenBufDirty[self.curScreen]:
- self.predraw( self.curScreen )
+ self.draw( self.curScreen )
TP.ProfileBegin( "TrackInterface::expose" )
@@ -771,11 +923,23 @@ class TrackInterface( gtk.EventBox ):
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, base = True ):
+ 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
@@ -783,7 +947,7 @@ class TrackInterface( gtk.EventBox ):
self.dirtyRectToAdd.height = height
#print "dirty %d %d %d %d %d %d" % (x, y, width, height, x+width, y+height)
- if page == self.curPage:
+ 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
@@ -792,13 +956,13 @@ class TrackInterface( gtk.EventBox ):
self.screenBufDirtyRect[self.curScreen].height = height
else:
self.screenBufDirtyRect[self.curScreen] = self.screenBufDirtyRect[self.curScreen].union( self.dirtyRectToAdd )
- self.screenBufResume[self.curScreen] = [0,0]
+ 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
- elif page == self.screenBufPage[self.preScreen]:
+ 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
@@ -806,7 +970,7 @@ class TrackInterface( gtk.EventBox ):
self.screenBufDirtyRect[self.preScreen].height = height
else:
self.screenBufDirtyRect[self.preScreen] = self.screenBufDirtyRect[self.preScreen].union( self.dirtyRectToAdd )
- self.screenBufResume[self.preScreen] = [0,0]
+ self.screenBufResume[self.preScreen] = [0,0,0]
self.screenBufDirty[self.preScreen] = True
#self.queue_draw()
diff --git a/Edit/TuneInterface.py b/Edit/TuneInterface.py
index e6790b9..d1d5481 100644
--- a/Edit/TuneInterface.py
+++ b/Edit/TuneInterface.py
@@ -58,6 +58,8 @@ class TuneInterface( gtk.EventBox ):
self.dragMode = None
self.dropAt = -1
+ self.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK)
+
self.connect( "size-allocate", self.size_allocated )
self.drawingArea.connect( "expose-event", self.draw )
self.connect( "button-press-event", self.handleButtonPress )
@@ -90,15 +92,23 @@ class TuneInterface( gtk.EventBox ):
def handleButtonPress( self, widget, event ):
ind = int(event.x-self.pageOffset)//self.pageSpacing
if ind >= self.noteDB.getPageCount():
- self.dragMode = self.DRAG_BLOCK
+ if self.dragMode != self.DRAG_MOVE:
+ self.dragMode = self.DRAG_BLOCK
return
if ind < 0: ind = 0
-
+
self.clickX = event.x
id = self.noteDB.getPageByIndex( ind )
-
- if event.type == gtk.gdk._2BUTTON_PRESS: # double click -> exclusive select
+
+ if event.state & gtk.gdk.BUTTON2_MASK:
+ # bring up properties or something
+ return
+
+ if event.type == gtk.gdk._3BUTTON_PRESS: # triple click -> select all
+ self.selectAll()
+ self.owner.displayPage( id )
+ elif event.type == gtk.gdk._2BUTTON_PRESS: # double click -> exclusive select
self.selectPage( id )
self.owner.displayPage( id )
else:
@@ -120,38 +130,54 @@ class TuneInterface( gtk.EventBox ):
self.owner.setContext( CONTEXT.PAGE )
def handleButtonRelease( self, widget, event ):
-
- if self.dragMode == self.DRAG_MOVE:
+ if self.dragMode == self.DRAG_MOVE \
+ and event.button == 1:
self.invalidate_rect( 0, 0, self.width, self.height ) # drop head
if self.dropAt > 0: after = self.noteDB.getPageByIndex( self.dropAt-1 )
else: after = False
self.noteDB.movePages( self.selectedIds, after )
+
self.dropAt = -1
- self.dragMode = None
+ self.dragMode = None
def handleMotion( self, widget, event ):
- if Config.ModKeys.ctrlDown and (self.dragMode == None or self.dragMode == self.DRAG_MOVE):
- self.dropAt = -1
- self.dragMode = self.DRAG_SELECT
-
- if self.dragMode == self.DRAG_SELECT: # select on drag
- ind = int(event.x-self.pageOffset)//self.pageSpacing
- if ind < self.noteDB.getPageCount(): self.selectPage( self.noteDB.getPageByIndex(ind), False )
- elif self.dragMode == self.DRAG_DESELECT: # deselect on drag
+ if event.is_hint:
+ x, y, state = self.window.get_pointer()
+ event.x = float(x)
+ event.y = float(y)
+ event.state = state
+
+ if event.state & gtk.gdk.BUTTON1_MASK: # clicking
+ if Config.ModKeys.ctrlDown and (self.dragMode == None or self.dragMode == self.DRAG_MOVE):
+ self.dropAt = -1
+ self.dragMode = self.DRAG_SELECT
+
+ if self.dragMode == self.DRAG_SELECT: # select on drag
+ ind = int(event.x-self.pageOffset)//self.pageSpacing
+ if ind < self.noteDB.getPageCount(): self.selectPage( self.noteDB.getPageByIndex(ind), False )
+ elif self.dragMode == self.DRAG_DESELECT: # deselect on drag
+ ind = int(event.x-self.pageOffset)//self.pageSpacing
+ if ind < self.noteDB.getPageCount(): self.deselectPage( self.noteDB.getPageByIndex(ind) )
+ elif self.dragMode == None and abs(self.clickX-event.x) > 20: # drag and drop
+ self.dragMode = self.DRAG_MOVE
+
+ if self.dragMode == self.DRAG_MOVE:
+ self.dropAt = int(event.x-self.pageOffset+Config.PAGE_THUMBNAIL_WIDTH_DIV2)//self.pageSpacing
+ c = self.noteDB.getPageCount()
+ if self.dropAt > c: self.dropAt = c
+ self.invalidate_rect( 0, 0, self.width, self.height )
+
+ else: # hovering
ind = int(event.x-self.pageOffset)//self.pageSpacing
- if ind < self.noteDB.getPageCount(): self.deselectPage( self.noteDB.getPageByIndex(ind) )
- elif self.dragMode == None and abs(self.clickX-event.x) > 20: # drag and drop
- self.dragMode = self.DRAG_MOVE
+ if 0 <= ind < self.noteDB.getPageCount():
+ id = self.noteDB.getPageByIndex(ind)
+ if id != self.displayedPage:
+ self.owner.predrawPage( id )
- if self.dragMode == self.DRAG_MOVE:
- self.dropAt = int(event.x-self.pageOffset+Config.PAGE_THUMBNAIL_WIDTH_DIV2)//self.pageSpacing
- c = self.noteDB.getPageCount()
- if self.dropAt > c: self.dropAt = c
- self.invalidate_rect( 0, 0, self.width, self.height )
def displayPage( self, id, scroll ):
if self.displayedPage == id: return -1
@@ -160,12 +186,12 @@ class TuneInterface( gtk.EventBox ):
if id not in self.selectedIds:
self.selectPage( id )
-
+
ind = self.noteDB.getPageIndex( id )
-
+
startX = self.pageOffset + ind*self.pageSpacing
stopX = startX + self.pageSpacing
-
+
self.invalidate_rect( 0, 0, self.width, self.height )
if scroll > startX: scroll = startX
@@ -176,7 +202,7 @@ class TuneInterface( gtk.EventBox ):
return -1
else:
scroll = stopX - self.baseWidth
-
+
return scroll
def selectPage( self, id, exclusive = True ):
@@ -213,11 +239,14 @@ class TuneInterface( gtk.EventBox ):
return True # page removed from the selection
+ def selectAll( self ):
+ self.selectedIds = self.noteDB.getTune()[:]
+ self.invalidate_rect( 0, 0, self.width, self.height )
+
def clearSelection( self ):
self.selectedIds = []
-
self.invalidate_rect( 0, 0, self.width, self.height )
-
+
def getSelectedIds( self ):
return self.selectedIds
@@ -249,7 +278,7 @@ class TuneInterface( gtk.EventBox ):
# Drawing
def draw( self, drawingArea, event ):
-
+
startX = event.area.x
startY = event.area.y
stopX = event.area.x + event.area.width
diff --git a/Util/NoteDB.py b/Util/NoteDB.py
index ff6f2d4..257c8a2 100644
--- a/Util/NoteDB.py
+++ b/Util/NoteDB.py
@@ -50,6 +50,9 @@ class NoteDB:
self.nextId = 0 # base id, first page will be 1
+ self.clipboard = [] # stores copied cs notes
+ self.clipboardArea = [] # stores the limits and tracks for each page in the clipboard
+
#-- private --------------------------------------------
def _genId( self ):
self.nextId += 1
@@ -224,16 +227,21 @@ class NoteDB:
# ... up to N
# page id or -1 to exit
def addNotes( self, stream ):
+ new = {}
i = [-1]
p = self._readstream(stream,i)
while p != -1:
+ if p not in new:
+ new[p] = [ [] for x in range(Config.NUMBER_OF_TRACKS) ]
t = self._readstream(stream,i)
N = self._readstream(stream,i)
hint = [0]
for j in range(N):
- self.addNote( p, t, self._readstream(stream,i), hint )
+ new[p][t].append( self.addNote( p, t, self._readstream(stream,i), hint ) )
p = self._readstream(stream,i)
+ return new
+
def deleteNote( self, page, track, id ):
ind = self.noteS[page][track].index( self.noteD[page][track][id] )
@@ -265,14 +273,17 @@ class NoteDB:
self.deleteNote( p, t, self._readstream(stream,i) )
p = self._readstream(stream,i)
- def deleteNotesByTrack( self, page, track ):
- notes = self.noteS[page][track][:]
- for n in notes:
- self.deleteNote( page, track, n.id )
+ def deleteNotesByTrack( self, pages, tracks ):
+ for p in pages:
+ for t in tracks:
+ notes = self.noteS[p][t][:]
+ for n in notes:
+ self.deleteNote( p, t, n.id )
def duplicateNote( self, page, track, id, toPage, toTrack, offset ):
cs = self.noteD[page][track][id].cs.clone()
- cs.track = toTrack
+ cs.trackId = toTrack
+ cs.pageId = toPage
cs.onset += offset
ticks = self.pages[toPage].ticks
if cs.onset >= ticks: return False # off the end of the page
@@ -340,7 +351,7 @@ class NoteDB:
param = self._readstream(stream,i)
N = self._readstream(stream,i)
for j in range(N):
- self.updateNote( p, t, self._readstream(stream,i), param, stream[inc(i)] )
+ self.updateNote( p, t, self._readstream(stream,i), param, self._readstream(stream,i) )
p = self._readstream(stream,i)
#-- private --------------------------------------------
@@ -377,6 +388,136 @@ class NoteDB:
#=======================================================
+ # Clipboard Functions
+
+ # stream format:
+ # page id
+ # track index
+ # number of following notes (N)
+ # note id
+ # ... up to N
+ # page id or -1 to exit
+ def notesToClipboard( self, stream ):
+ self.clipboard = []
+ self.clipboardArea = []
+ i = [-1]
+ pages = []
+ p = self._readstream(stream,i)
+ while p != -1:
+ if p not in pages:
+ page = [ [] for x in range(Config.NUMBER_OF_TRACKS) ]
+ pageArea = { "limit": [ 99999, 0 ],
+ "tracks": [ 0 for x in range(Config.NUMBER_OF_TRACKS) ] }
+ pages.append(p)
+ self.clipboard.append(page)
+ self.clipboardArea.append(pageArea)
+ else:
+ ind = pages.index(p)
+ page = self.clipboard[ind]
+ pageArea = self.clipboardArea[ind]
+ t = self._readstream(stream,i)
+ pageArea["tracks"][t] = 1
+ N = self._readstream(stream,i)
+ for j in range(N):
+ cs = self.noteD[p][t][self._readstream(stream,i)].cs.clone()
+ if cs.onset < pageArea["limit"][0]: pageArea["limit"][0] = cs.onset
+ if cs.onset + cs.duration > pageArea["limit"][1]: pageArea["limit"][1] = cs.onset + cs.duration
+ page[t].append( cs )
+ p = self._readstream(stream,i)
+
+ return self.clipboardArea
+
+ def tracksToClipboard( self, pages, tracks ):
+ self.clipboard = []
+ self.clipboardOrigin = [ 0, 0 ]
+ self.clipboardArea = []
+ for p in pages:
+ page = [ [] for x in range(Config.NUMBER_OF_TRACKS) ]
+ pageArea = { "limit": [ 0, 99999 ],
+ "tracks": [ 0 for x in range(Config.NUMBER_OF_TRACKS) ] }
+ self.clipboard.append(page)
+ self.clipboardArea.append(pageArea)
+ for t in tracks:
+ pageArea["tracks"][t] = 1
+ for id in self.noteD[p][t]:
+ cs = self.noteD[p][t][id].cs.clone()
+ page[t].append( cs )
+
+ return self.clipboardArea
+
+ # trackMap = { X: Y, W: Z, ... }; X,W are track indices, Y,Z are clipboard indices
+ def pasteClipboard( self, pages, offset, trackMap ):
+ if not len(self.clipboard): return
+
+ deleteStream = []
+ updateStream = []
+ addStream = []
+
+ pp = 0
+ ppMax = len(self.clipboard)
+ for p in pages:
+ ticks = self.pages[p].ticks
+ area = self.clipboardArea[pp]
+ area["limit"][0] += offset
+ area["limit"][1] += offset
+ for t in trackMap.keys():
+ if not area["tracks"][trackMap[t]]: continue
+ tdeleteStream = []
+ tupdateOStream = []
+ tupdateDStream = []
+ taddStream = []
+ # clear area
+ for n in self.noteS[p][t]:
+ start = n.cs.onset
+ end = start + n.cs.duration
+ if area["limit"][0] <= start < area["limit"][1]: start = area["limit"][1]
+ if area["limit"][0] < end <= area["limit"][1]: end = area["limit"][0]
+ if start < area["limit"][0] and end > area["limit"][1]: end = area["limit"][0]
+ if end <= start:
+ tdeleteStream.append( n.id )
+ elif start != n.cs.onset:
+ tupdateDStream += [ n.id, end - start ]
+ tupdateOStream += [ n.id, start ]
+ elif end != start + n.cs.duration:
+ tupdateDStream += [ n.id, end - start ]
+ if len(tdeleteStream):
+ deleteStream += [ p, t, len(tdeleteStream) ] + tdeleteStream
+ if len(tupdateOStream):
+ updateStream += [ p, t, PARAMETER.ONSET, len(tupdateOStream)//2 ] + tupdateOStream
+ if len(tupdateDStream):
+ updateStream += [ p, t, PARAMETER.DURATION, len(tupdateDStream)//2 ] + tupdateDStream
+ # paste notes
+ for cs in self.clipboard[pp][trackMap[t]]:
+ newcs = cs.clone()
+ newcs.onset += offset
+ if newcs.onset >= ticks: continue
+ if newcs.onset + newcs.duration > ticks:
+ newcs.duration = ticks - newcs.onset
+ newcs.pageId = p
+ newcs.trackId = t
+ # TODO update the cs.instrument or any other parameters?
+ taddStream.append( newcs )
+ if len(taddStream):
+ addStream += [ p, t, len(taddStream) ] + taddStream
+
+ pp += 1
+ if pp == ppMax: pp -= ppMax
+
+ if len(deleteStream):
+ self.deleteNotes( deleteStream + [-1] )
+ if len(updateStream):
+ self.updateNotes( updateStream + [-1] )
+ if len(addStream):
+ return self.addNotes( addStream + [-1] )
+
+ return None
+
+ def getClipboardArea( self, ind ):
+ N = len(self.clipboardArea)
+ while ind >= N: ind -= N
+ return self.clipboardArea[ind]
+
+ #=======================================================
# Listener Functions
def addListener( self, listener, parasite = None, page = False, note = False ):
@@ -438,6 +579,11 @@ class NoteDB:
def getPageIndex( self, page ):
return self.tune.index(page)
+ def getNote( self, page, track, id, listener = None ):
+ if listener:
+ return self.parasiteD[page][track][listener][id]
+ return self.noteD[page][track][id]
+
def getNotesByPage( self, page, listener = None ):
notes = []
if listener:
diff --git a/scripts/clean_trailing_whitespace.py b/scripts/clean_trailing_whitespace.py
index 13532b2..57e793c 100644
--- a/scripts/clean_trailing_whitespace.py
+++ b/scripts/clean_trailing_whitespace.py
@@ -15,7 +15,7 @@ r = open(bakName)
w = open(fName, "w" )
for line in r:
line = line[:-1]
- while len(line) and (line[-1] == " " or line[-1] == " "):
+ while len(line) and (line[-1] == " " or line[-1] == " "):
line = line[:-1]
w.write(line+"\n")