Browse Source

implement the four noteSorting functions that were already in the menus

master
Nils 5 years ago
parent
commit
eff6f32d4d
  1. 169
      engine/api.py
  2. 16
      engine/items.py
  3. 2
      engine/main.py
  4. 9
      qtgui/menu.py

169
engine/api.py

@ -25,6 +25,7 @@ import logging; logging.info("import {}".format(__file__))
#Python Standard Library
import sys
import random
random.seed()
from typing import Iterable, Callable, Tuple
#Third Party Modules
@ -381,34 +382,37 @@ def _createLambdaMoveToForCurrentPosition():
moveFunction = lambda trIdx=trackIndex, blIdx=blockIndex, curIdx=localCursorIndexInBlock, pitchIdx=session.data.cursor.pitchindex: moveTo(trIdx, blIdx, curIdx, pitchIdx)
return moveFunction
#The following function is not neccesary. A selection is a user-created thing, not something you need for internal processing, especially not for undo.
#Leave it as reminder.
#def _createLambdaRecreateSelection():
# """for undo only
# Call it when you still have a selection, right after you do the
# processing you want to undo.
#
# A valid selection implies that the cursor is on one end
# of the selection. It doesn't matter which one but for the
# sake of consistency and convenience we make sure that we end up with
# the cursor on the bottomRight position.
# It is entirely possible that the selection changed
# the duration and the dimensions. The bottom right item might not
# be on the same tickindex nor does it need to exist anymore.
# Only the topLeft position is guaranteed to exist and have the same
# tickindex. The item on that position however may not be there
# anymore.
# """
# def recreateSelection(moveToFunction, topLeftCursor, bottomRightCursor):
# session.data.cursorWhenSelectionStarted = None #take care of the current selection
# moveToFunction()
# session.data.goTo(topLeftCursor["trackIndex"], topLeftCursor["blockindex"], topLeftCursor["localCursorIndex"])
# session.data.setSelectionBeginning()
# session.data.goTo(bottomRightCursor["trackIndex"], bottomRightCursor["blockindex"], bottomRightCursor["localCursorIndex"])
#
# assert session.data.cursorWhenSelectionStarted #only call this when you still have a selection
# createSelectionFunction = lambda moveTo=_createLambdaMoveToForCurrentPosition(), tL=session.data.cursor.exportObject(session.data.currentTrack().state), bR=session.data.cursorWhenSelectionStarted, : recreateSelection(moveTo, tL, bR)
# return createSelectionFunction #When this function gets called we are back in the position that the newly generated data is selected, ready to to the complementary processing.
def _createLambdaRecreateSelection():
"""Not for undo.
Call it when you still have a selection, right after you do the
processing.
This is to keep a selection that changed the item positions but you want to support a new
call to the function immediately. For example the reorder notes by shuffling function.
The user will most likely hit that several times in a row to find something nice.
A valid selection implies that the cursor is on one end
of the selection. It doesn't matter which one but for the
sake of consistency and convenience we make sure that we end up with
the cursor on the bottomRight position.
It is entirely possible that the selection changed
the duration and the dimensions. The bottom right item might not
be on the same tickindex nor does it need to exist anymore.
Only the topLeft position is guaranteed to exist and have the same
tickindex. The item on that position however may not be there
anymore.
"""
def recreateSelection(moveToFunction, topLeftCursor, bottomRightCursor):
session.data.cursorWhenSelectionStarted = None #take care of the current selection
moveToFunction()
session.data.goTo(topLeftCursor["trackIndex"], topLeftCursor["blockindex"], topLeftCursor["localCursorIndex"])
session.data.setSelectionBeginning()
session.data.goTo(bottomRightCursor["trackIndex"], bottomRightCursor["blockindex"], bottomRightCursor["localCursorIndex"])
assert session.data.cursorWhenSelectionStarted #only call this when you still have a selection
createSelectionFunction = lambda moveTo=_createLambdaMoveToForCurrentPosition(), tL=session.data.cursor.exportObject(session.data.currentTrack().state), bR=session.data.cursorWhenSelectionStarted, : recreateSelection(moveTo, tL, bR)
return createSelectionFunction #When this function gets called we are back in the position that the newly generated data is selected, ready to to the complementary processing.
def _updateCallbackForListOfTrackIDs(listOfChangedTrackIds):
@ -497,7 +501,7 @@ def duplicate(): #ctrl+d
if customBuffer: #success
session.data.goToSelectionStart()
pos = session.data.where() #where even keeps the local position if a content linked block inserts items before our position
pasteObjects(customBuffer = customBuffer, updateCursor = False, overwriteSelection = False)
pasteObjects(customBuffer = customBuffer, updateCursor = False, overwriteSelection = False) #handles undo
session.data.goTo(*pos)
callbacks._setCursor(destroySelection=False)
else:
@ -2311,8 +2315,113 @@ def insertRandomFromClipboard():
item = items.createChordOrRest(session.data.cursor.prevailingBaseDuration, random.choice(pool).copy().pitchlist())
insertItem(item)
#Midi only
#Ordering
def _listOChordsFromSelection():
"""Returns (None, None) or a tuple(listOfChangedTrackIds, list of notelists (per track))"""
if session.data.cursorWhenSelectionStarted:
validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = session.data.listOfSelectedItems(removeContentLinkedData=True)
if not validSelection:
return None, None
chordlists = []
for track in selectedTracksAndItems:
chordlists.append(list())
#[(object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), ...], #track 1
#[(object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), ...], #track 2
if len(track) >= 2:
for item, keysig in track:
if type(item) is items.Chord:
chordlists[-1].append(item)
if not tuple(flatList(chordlists)):
return None, None
return listOfChangedTrackIds, chordlists
else:
return None, None
def _reorderChords(functionToGenerateChordOrder, descriptionString):
"""Works track by track.
It is assumed that each chord is only once in generatorOfChords and also exists in the score
"""
listOfChangedTrackIds, chordlists = _listOChordsFromSelection()
if not listOfChangedTrackIds:
return
orderBeforeInsert = session.data.getBlockAndItemOrder()
moveFunction = _createLambdaMoveToForCurrentPosition()
def registeredUndoFunction():
moveFunction()
_changeBlockAndItemOrder(orderBeforeInsert)
session.history.register(registeredUndoFunction, descriptionString)
seentest = set()
recreateSelection = _createLambdaRecreateSelection()
for track in chordlists: #read-only
generatorOfChords = functionToGenerateChordOrder(track)
#We need to gather the indices of the original items all at once.
#Otherwise, if we replace step by step, we will find items multiple times leading to little or no shuffling.
originalOrder = [] #tuples. we delete from the original track so the indices will be all wrong in the end, except the first! and after we insert a new item at that first position the second index will be correct again. and so on...
for chord in track: #iterate over the original order
#Replace each chord with a random one by removing the original chord from block data. This keeps the chord objects unchanged, ready for undo by simply reordering.
assert not chord in seentest #shared between all tracks.
seentest.add(chord)
data = next(bl for bl in chord.parentBlocks).data #just take any block. data is shared between all of them. Content linked data was removed from the selection
index = data.index(chord)
originalOrder.append((data, index, chord))
for chord in track:
data.remove(chord)
for data, index, chord in originalOrder:
new = next(generatorOfChords)
data.insert(index, new)
callbacks._historyChanged()
_updateCallbackForListOfTrackIDs(listOfChangedTrackIds)
recreateSelection() #the bottom right position is now at a different position but we want the selection to stay the same to call reorder again
callbacks._setCursor(destroySelection = False)
def reorderShuffle():
"""Only for valid selections.
Works track by track"""
def functionToGenerateChordOrder(track):
shuffledTrack = random.sample(track, len(track))
shuffledTrack = (chrd for chrd in shuffledTrack) #make generator so we can use next()
return shuffledTrack
_reorderChords(functionToGenerateChordOrder, descriptionString="Shuffle Notes") #handles undo and callbacks
def reorderReverse():
"""Only for valid selections.
Works track by track"""
def functionToGenerateChordOrder(track):
return reversed(track) #already a generator
_reorderChords(functionToGenerateChordOrder, descriptionString="Reverse Notes") #handles undo and callbacks
def reorderAscending():
"""Only for valid selections.
Works track by track"""
def functionToGenerateChordOrder(track):
asc = sorted(track, key=lambda item: item.notelist[0].pitch) #notelist[0] is the lowest pitch in a chord
return (chrd for chrd in asc) #make generator
_reorderChords(functionToGenerateChordOrder, descriptionString="Shuffle Notes") #handles undo and callbacks
def reoderdDescending():
"""Only for valid selections.
Works track by track"""
def functionToGenerateChordOrder(track):
asc = sorted(track, key=lambda item: item.notelist[0].pitch) #notelist[0] is the lowest pitch in a chord
return reversed(asc) #already a generator
_reorderChords(functionToGenerateChordOrder, descriptionString="Shuffle Notes") #handles undo and callbacks
#Midi only
def instrumentChange(program, msb, lsb, shortInstrumentName):
change = items.InstrumentChange(program, msb, lsb, shortInstrumentName)
insertItem(change)

16
engine/items.py

@ -661,7 +661,7 @@ class Item(object):
def __init__(self):
self.duration = self.pseudoDuration
self.notelist = []
self.notelist = [] #only in Chord. Here for compatibility reasons
self.parentBlocks = WeakSet() #this is filled and manipulated by the Block Class through copy, split, contentLink etc. this takes also care of Item.copy(). Don't look in this file for anything parentBlocks related.
#self._secondInit(parentBlock = None) #Items don't know their parent object. They are in a mutable list which can be in several containers.
self.lilypondParameters = { # Only use basic data types! Not all lilypond parameters are used by every item. For clarity and editing we keep them all in one place.
@ -930,7 +930,7 @@ class Chord(Item):
def __init__(self, firstDuration, firstPitch):
super().__init__()
firstNote = Note(self, firstDuration, firstPitch)
self.notelist = [firstNote]
self.notelist = [firstNote] #from lowest to highest
self._beamGroup = False # Works a bit like legato slurs, only this is a property of a note, not an item. self.beamGroup is a @property that self-corrects against errors.
self._secondInit(parentBlock = None) #see Item._secondInit.
self.midiChannelOffset = 0 #from -15 to 15. In reality much less. Expected values are -3 to +3
@ -991,6 +991,12 @@ class Chord(Item):
self.durationGroup.cacheMinimumNote()
return lambda: self._setNotelist(oldNotelist)
def replaceNoteList(self, notelist):
"""Wants an actual notelist, not pitches.
This is its own function because the private _setNotelist was created years before this here
"""
return self._setNotelist(notelist)
def addNote(self, pitch):
"""The notelist gets sorted after insert.
Each white note is only allowed once in each chord.
@ -1288,6 +1294,7 @@ class Chord(Item):
self._cachedClefForLedgerLines = None
return lambda: self._setPitchlist(oldValues)
def sharpen(self):
oldValues = []
for note in self.notelist:
@ -1488,6 +1495,7 @@ class Chord(Item):
self.notelist.sort()
self._cachedClefForLedgerLines = None
return lambda: self._setPitchlist(oldValues)
def moreDuration(self):
oldValues = []
@ -2235,8 +2243,12 @@ class Clef(Item):
"""Since we implemented __eq__ we need to make Clef hashable again"""
return id(self)
def __eq__(self, other): # self == other
if not type(other) is Clef:
return False
return self.clefString == other.clefString
def __ne__(self, other): # self != other
if not type(other) is Clef:
return True
return self.clefString != other.clefString

2
engine/main.py

@ -402,7 +402,7 @@ class Data(template.engine.sequencer.Score):
track.toPosition(originalPosition) #has head() in it
return (False, topLeft, bottomRight)
def listOfSelectedItems(self, removeContentLinkedData):
def listOfSelectedItems(self, removeContentLinkedData:bool):
"""
validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = returnedStuff

9
qtgui/menu.py

@ -287,10 +287,11 @@ class MenuActionDatabase(object):
self.mainWindow.ui.actionMirror_around_Cursor: api.mirrorAroundCursor,
#Note Sorting
self.mainWindow.ui.actionRandom: api.nothing,
self.mainWindow.ui.actionReverse: api.nothing,
self.mainWindow.ui.actionAscending: api.nothing,
self.mainWindow.ui.actionDescending: api.nothing,
self.mainWindow.ui.actionRandom: api.reorderShuffle,
self.mainWindow.ui.actionReverse: api.reorderReverse,
self.mainWindow.ui.actionAscending: api.reorderAscending,
self.mainWindow.ui.actionDescending: api.reoderdDescending,
#Midi
self.mainWindow.ui.actionInstrument_Change: SecondaryProgramChangeMenu(self.mainWindow), #no lambda for submenus. They get created here once and have a __call__ option that executes them. There is no internal state in these menus.
self.mainWindow.ui.actionChannel_Change: SecondaryChannelChangeMenu(self.mainWindow), #no lambda for submenus. They get created here once and have a __call__ option that executes them. There is no internal state in these menus.

Loading…
Cancel
Save