#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), Laborejo2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library #Third party from PyQt5 import QtCore, QtGui, QtWidgets translate = QtCore.QCoreApplication.translate #Template #Our own files import engine.api as api from .constantsAndConfigs import constantsAndConfigs from .grid import GuiGrid from .conductor import Conductor, ConductorTransparentBlock from .musicstructures import GuiBlockHandle, GuiTrack, sceneXOffsetForInitialSigs from .graphs import CCGraphTransparentBlock, CCPath from .cursor import Cursor, Playhead, Selection class GuiScore(QtWidgets.QGraphicsScene): def __init__(self, parentView): super().__init__() self.parentView = parentView self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) self.tracks = {} #trackId:guiTrack, #if we don't save the instances here in Python space Qt will loose them and they will not be displayed without any error message. self.deleteOnIdleStack = set() # a stack that holds hidden items that need to be deleted. Hiding is much faster than deleting so we use that for the blocking function. self._deleteOnIdleLoop = QtCore.QTimer() self._deleteOnIdleLoop.start(100) #0 means "if there is time" self._deleteOnIdleLoop.timeout.connect(self._deleteOnIdle) #processes deleteOnIdleStack self.duringTrackDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a track is moved around by the mouse self.duringBlockDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a block is moved around by the mouse self.mouseMoveEventBlocked = False #mouseMoveEvent and helper functions self.conductor = Conductor(parentView = self.parentView) self.addItem(self.conductor) self.conductor.setPos(0, -1 * self.conductor.totalHeight) self.yStart = self.conductor.y() - self.conductor.totalHeight/2 self.hiddenTrackCounter = QtWidgets.QGraphicsSimpleTextItem("") #filled in by self.redraw on callback tracksChanged (loading or toggling visibility of backend tracks) self.addItem(self.hiddenTrackCounter) self.backColor = QtGui.QColor() self.backColor.setNamedColor("#fdfdff") self.setBackgroundBrush(self.backColor) self.cachedSceneHeight = 0 #set in self.redraw. Used by updateTrack to set the sceneRect self.grid = GuiGrid(parentScene=self) self.addItem(self.grid) self.grid.setPos(0, -20 * constantsAndConfigs.stafflineGap) #this is more calculation than simply using self.yStart, and might require manual adjustment in the future, but at least it guarantees the grid matches the staffline positions self.grid.setZValue(-50) #All Cursors self.cursor = Cursor() self.addItem(self.cursor) self.cursor.setZValue(90) self.selection = Selection() self.selection.setZValue(80) self.addItem(self.selection) self.playhead = Playhead(self) self.addItem(self.playhead) self.playhead.setY(self.yStart) self.playhead.setZValue(100) #Callbacks api.callbacks.tracksChanged.append(self.redraw) api.callbacks.updateTrack.append(self.updateTrack) api.callbacks.updateBlockTrack.append(self.trackPaintBlockBackgroundColors) api.callbacks.updateGraphTrackCC.append(self.updateGraphTrackCC) api.callbacks.updateGraphBlockTrack.append(self.updateGraphBlockTrack) api.callbacks.graphCCTracksChanged.append(self.syncCCsToBackend) def updateMode(self, nameAsString): assert nameAsString in constantsAndConfigs.availableEditModes for track in self.tracks.values(): track.updateMode(nameAsString) self.grid.updateMode(nameAsString) self.reactToHorizontalScroll(0) #once force all block mode track names into the center. We cannot do this in init because the tracks are not ready by then. This assumes we always start the program in notation mode def updateModeSingleTrackRedraw(self, nameAsString:str, trackId:int, trackExport:tuple): """trackExport is a tuple of block export dicts""" if trackId in self.tracks: #is visible? self.tracks[trackId].updateMode(nameAsString) def maxTrackLength(self): if self.tracks: return max(tr.lengthInPixel for tr in self.tracks.values()) #return max(max(tr.lengthInPixel for tr in self.tracks.values()), self.parentView.geometry().width()) #return max(max(tr.lengthInPixel for tr in self.scene().tracks.values()), self.scene().parentView.geometry().width()) + self.scene().parentView.geometry().width() else: return 0 #self.parentView.geometry().width() def updateSceneRect(self): #self.parentView.setSceneRect(QtCore.QRectF(-5, self.yStart, self.maxTrackLength() + 300, self.cachedSceneHeight)) #x,y,w,h self.parentView.setSceneRect(QtCore.QRectF(sceneXOffsetForInitialSigs, self.yStart, self.maxTrackLength() + 300 + abs(sceneXOffsetForInitialSigs), self.cachedSceneHeight)) #x,y,w,h def updateTrack(self, trackId, staticRepresentationList): """for callbacks""" if trackId in self.tracks: self.tracks[trackId].redraw(staticRepresentationList) #else: #hidden track. But this can still happen through the data editor self.grid.reactOnScoreChange() self.updateSceneRect() def trackPaintBlockBackgroundColors(self, trackId, staticBlocksRepresentation): if trackId in self.tracks: self.tracks[trackId].paintBlockBackgroundColors(staticBlocksRepresentation) #else: #hidden track. def updateGraphTrackCC(self, trackId, ccNumber, staticRepresentationList): """TrackId is a real notation track which has a dict of CCs""" if trackId in self.tracks: track = self.tracks[trackId] ccPath = track.ccPaths[ccNumber] ccPath.createGraphicItemsFromData(staticRepresentationList) def updateGraphBlockTrack(self, trackId, ccNumber, staticRepresentationList): """TrackId is a real notation track which has a dict of CCs""" if trackId in self.tracks: self.tracks[trackId].ccPaths[ccNumber].updateGraphBlockTrack(staticRepresentationList) def toggleNoteheadsRectangles(self): for track in self.tracks.values(): track.toggleNoteheadsRectangles() def syncCCsToBackend(self, trackId, listOfCCsInThisTrack): """Delete ccGraphs that are gui-only, create gui versions of graphs that are backend-only. Don't touch the others. This is the entry point for new CCPaths. They get created only here. """ #TODO: and moved from one CC value to another? guiCCs = set(self.tracks[trackId].ccPaths.keys()) for backendCC in listOfCCsInThisTrack: if backendCC in guiCCs: guiCCs.remove(backendCC) #all right. no update needed. else: #new CC. not existent in the Gui yet. Create. #This is the place where we create new CCPaths new = CCPath(parentGuiTrack = self.tracks[trackId], parentDataTrackId = trackId) self.tracks[trackId].ccPaths[backendCC] = new #store in the GUI Track new.setParentItem(self.tracks[trackId]) new.setPos(0,0) #new.setZValue(100) #all items left in the set are GUI-only CCs. which means the backend graphs were deleted. Delete them here as well. for cc in guiCCs: self.removeWhenIdle(self.tracks[trackId].ccPaths[cc]) del self.tracks[trackId].ccPaths[cc] def redraw(self, listOfStaticTrackRepresentations): """The order of guiTracks depends on the backend index. This way it is a no-brainer, we don't need to maintain our own order, just sync with the backend when the callback comes in. Also handles meta data like track names. But not the actual track content, which is done through self.updateTrack which has its own api-callback called by callbacksDatabase.tracksChanged""" for track in self.tracks.values(): track.hide() doubleTrackOffset = 0 for trackExportObject in listOfStaticTrackRepresentations: if not trackExportObject["id"] in self.tracks: guiTrack = GuiTrack(self, trackExportObject) self.tracks[trackExportObject["id"]] = guiTrack self.addItem(guiTrack) guiTrack.secondStageInitNowThatWeHaveAScene() self.tracks[trackExportObject["id"]].staticExportItem = trackExportObject self.tracks[trackExportObject["id"]].setPos(0, constantsAndConfigs.trackHeight * trackExportObject["index"] + doubleTrackOffset) self.tracks[trackExportObject["id"]].setZValue(0) #direct comparison only possible with the grid, which is at -50 self.tracks[trackExportObject["id"]].nameGraphic.updateName(trackExportObject) self.tracks[trackExportObject["id"]].blockModeNameGraphic.updateName(trackExportObject) self.tracks[trackExportObject["id"]].show() if trackExportObject["double"]: doubleTrackOffset += constantsAndConfigs.trackHeight toDelete = [] for trackId, track in self.tracks.items(): if not track.isVisible(): toDelete.append((trackId, track)) for trackId_, track_ in toDelete: self.removeWhenIdle(track_) del self.tracks[trackId_] #Finally, under the last track, tell the user how many hidden tracks there are nrOfHiddenTracks = len(api.session.data.hiddenTracks) if nrOfHiddenTracks: self.hiddenTrackCounter.setText(translate("scorescene", "… and {} hidden tracks").format(nrOfHiddenTracks)) else: #remove previous status message self.hiddenTrackCounter.setText("") belowLastTrack = constantsAndConfigs.trackHeight * (trackExportObject["index"] + 1) + doubleTrackOffset self.hiddenTrackCounter.setPos(5, belowLastTrack) self.cachedSceneHeight = belowLastTrack + constantsAndConfigs.trackHeight def removeWhenIdle(self, item): """Call this function instead of removeItem. You are responsible to delete the item from any list or other container where it was stored in the meantime yourself. Other methods tried: -removeItem much slower -create second scene and do scene2.addItem to get it out here - same as removeItem, if not slower -delete a track and recreate a new one - same as all removeItems -just hide the track and create a new one. slightly slower as hiding just the items. -item.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents) slow, but not as slow as removeItem -just hide items, never delete them is fast but gets slower the more items are hidden in the track """ item.hide() self.deleteOnIdleStack.add(item) #This will actually delete the item and remove it from the scene when there is idle time def _deleteOnIdle(self): """Hiding a QGraphicsItem is much faster than removing it from the scene. To keep the GUI responsive but also to get rid of the old data we hide in createGraphicItemsFromData and whenever there is time the actual removing and deleting will happen here. This function is connected to a timer defined in self init. """ #if self.deleteOnIdleStack: # deleteMe = self.deleteOnIdleStack.pop() # self.removeItem(deleteMe) #This is the only line in the program that should call scene.removeItem # del deleteMe for deleteMe in self.deleteOnIdleStack: self.removeItem(deleteMe) #This is the only line in the program that should call scene.removeItem self.deleteOnIdleStack = set() def trackAt(self, qScenePosition): """trackAt always returns the full GuiTrack, even if in ccEdit mode.""" if qScenePosition.y() < self.conductor.y() + self.conductor.totalHeight/4: return self.conductor for guiTrack in sorted(self.tracks.values(), key = lambda gT: gT.y()): if guiTrack.y() >= qScenePosition.y() - constantsAndConfigs.trackHeight/2: return guiTrack else: return None #no track here. def blockAt(self, qScenePosition): track = self.trackAt(qScenePosition) if track is self.conductor: return self.conductor.blockAt(qScenePosition.x()) if self.parentView.mode() in ("block", "notation"): if track: return track.blockAt(qScenePosition.x()) elif self.parentView.mode() == "cc": if track and constantsAndConfigs.ccViewValue in track.ccPaths: return track.ccPaths[constantsAndConfigs.ccViewValue].blockAt(qScenePosition.x()) else: raise NotImplementedError return None def notTrueAnymore_wheelEvent(self, event): """ This docstring was either wrong in the first place or something changed in my code. However, we do not need to eat wheelEvent here anymore. On the contrary: It will block items, like the tempoItem from receiving wheelEvents. The commented out if/else for item detection also has a chance to work, but we don't need to test for that at all. Standard Qt-behavoiour is fine. What follows is the old docstring, for legacy documentation reasons: We MUST handle the event somehow. Otherwise background grid items will block the views(!) wheel scrolling, even when disabled and setting accepting mouse events to none. This is a qt bug that won't be fixed because API stability over correctnes (according to the bugtracker. Contrary to other parts of the system event.ignore and accept actually mean something. ignore will tell the caller to use the event itself, e.g. scroll. This event gets the wheel BEFORE the main window (zoom) """ #item = self.itemAt(event.scenePos(), self.parentView.transform()) #if type(item) is items.Note: #super().wheelEvent(event) #send to child item #else: event.ignore() #so the view scrolls or we zoom def stretchXCoordinates(self, factor): """Reposition the items on the X axis. Call goes through all parents/children, starting from ScoreView._stretchXCoordinates. Docstring there.""" #The big structures have a fixed position at (0,0) and move its child items, like notes, internally #Some items, like the cursor, move around, as a whole item, in the scene directly and need no stretchXCoordinates() themselves. self.grid.stretchXCoordinates(factor) self.conductor.stretchXCoordinates(factor) self.cursor.setX(self.cursor.pos().x() * factor) self.playhead.setX(self.playhead.pos().x() * factor) self.selection.stretchXCoordinates(factor) for track in self.tracks.values(): track.stretchXCoordinates(factor) self.updateSceneRect() #Macro-Structure: Score / Track / Block Moving and Duplicating """ def keyPressEvent(self, event): #Triggers only if there is no shortcut for an action. modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.AltModifier: Alt super().keyPressEvent(event) """ def mousePressEvent(self, event): """Pressing the mouse button is the first action of drag and drop. We make the mouse cursor invisible so the user can see where the object is moving """ if self.parentView.mode() == "block": #CC Block Moving is done in its own if clause below. if event.button() == QtCore.Qt.LeftButton: modifiers = QtWidgets.QApplication.keyboardModifiers() #Block Move if modifiers == QtCore.Qt.NoModifier: block = self.blockAt(event.scenePos()) if block: #works for note blocks and conductor blocks block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict. self.duringBlockDragAndDrop = block block.mousePressEventCustom(event) #Track Move elif (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier ) and self.parentView.mode() == "block": track = self.trackAt(event.scenePos()) if track and not track is self.conductor: #self.parentView.setCursor(QtCore.Qt.BlankCursor) self.cursor.hide() track.staticExportItem["guiPosStart"] = track.pos() self.duringTrackDragAndDrop = track elif event.button() == QtCore.Qt.RightButton: """Fake context menu event. The built-in one never got the correct detection. Yes, this will hardcode the context menu to right mouse button, which is not really the only way to trigger a context menu. However, too much time has been spent to get this right already. Good enough is better than good.""" block = self.blockAt(event.scenePos()) if block: #works for note blocks and conductor blocks block.contextMenuEventCustom(event) event.accept() #eat it return #CC is a special case because we need to handle points and block movement at the same time. #Hence the alt/shift key is mandatory here. That was also the reason why everything was with the middle mouse button AND shortcuts once, #but that was too inconvenient overall. This system is not 100% straightforward but easy enough to remember. And more robust against accidents. elif self.parentView.mode() == "cc" and event.button() == QtCore.Qt.LeftButton: modifiers = QtWidgets.QApplication.keyboardModifiers() if (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier): block = self.blockAt(event.scenePos()) if block and type(block) is CCGraphTransparentBlock: block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict. self.duringBlockDragAndDrop = block block.mousePressEventCustom(event) event.accept() #eat it return else: #CC mode but no modifiers. Add or move item. #implicit qt scene click detection was unreliable. In the end the manual route was the best option: block = self.blockAt(event.scenePos()) if block and type(block) is CCGraphTransparentBlock: #We are within the boundaries of a block super().mousePressEvent(event) #Apparently we need the double event call. Without this we select the wrong items, but not all the time. The result is not being able to move the cc user point. Just leave it in. Solving this "properly" would need refactoring all mouse events. if block.parentCCPath.hoveringOverItem: #this is maybe a CC User Item or block end marker. don't handle here, let the item handle it. super().mousePressEvent(event) return else: #Empty Block click block.parentCCPath.mousePressEventToAdd(event) return super().mousePressEvent(event) def _mmE_ensureCursorVisible(self, event): """Helper function for mouseMoveEvent. centerOn and ensureVisible either crash or they recurse forever or they scroll exponentially We must do it ourselves. Thanks, qt.""" if self.mouseMoveEventBlocked: return #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving. currentlyVisibleSceneRect = self.parentView.mapToScene(self.parentView.viewport().geometry()).boundingRect() #x, y top left corner, width, height leftEdge = int(currentlyVisibleSceneRect.x()) rightEdge = int(leftEdge + currentlyVisibleSceneRect.width()) topEdge = int(currentlyVisibleSceneRect.y()) bottomEdge = int(topEdge + currentlyVisibleSceneRect.height()) x = int(event.scenePos().x()) y = int(event.scenePos().y()) if x < leftEdge: self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving. self.parentView.horizontalScrollBar().setValue(self.parentView.horizontalScrollBar().value() - (leftEdge - x)) elif x > rightEdge: self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving. self.parentView.horizontalScrollBar().setValue(self.parentView.horizontalScrollBar().value() + (x - rightEdge)) if y < topEdge: self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving. self.parentView.verticalScrollBar().setValue(self.parentView.verticalScrollBar().value() - (topEdge - y)) elif y > bottomEdge: self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving. self.parentView.verticalScrollBar().setValue(self.parentView.verticalScrollBar().value() + (y - bottomEdge)) self.mouseMoveEventBlocked = False def mouseMoveEvent(self, event): """Catches certain mouse events for moving tracks and blocks. Otherwise the event is propagated to the real QGraphicsItem. Don't forget that an item needs to have the flag movable or selectable or else it will not get mouseRelease or mouseMove events. MousePress always works. This happens EVERY mouse move. Just idle mouse movement over the window. """ if self.duringTrackDragAndDrop: #X is locked for tracks. self._mmE_ensureCursorVisible(event) x = self.duringTrackDragAndDrop.staticExportItem["guiPosStart"].x() y = event.scenePos().y() self.duringTrackDragAndDrop.setPos(x, y) elif self.duringBlockDragAndDrop: #GuiBlockHandles are children of a Track. Their Y position will be offset by the track when we move them around with the mouse. #Which is the same the distance to the first track is on top. We need to substract that for a good look and feel. #The actual later dropping of the block is handled with the mouse cursor coordinates, the moving colored block is just eyecandy. self._mmE_ensureCursorVisible(event) x = event.scenePos().x() y = event.scenePos().y() - self.duringBlockDragAndDrop.parentGuiTrack.y() self.duringBlockDragAndDrop.setPos(x, y) else: super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): """Catches certain mouse events for moving tracks and blocks. Otherwise the event is propagated to the real QGraphicsItem. Don't forget that an item needs to have the flag movable or selectable or else it will not get mouseRelease or mouseMove events. MousePress always works.""" #self.parentView.unsetCursor() #While moving stuff the mouse-cursor is hidden. Reset. #self.cursor.show() #Our own position cursor #TODO: why was that in here? It shows the cursor after a mouseclick, even in CC mode (it should not) tempBlockDragAndDrop = self.duringBlockDragAndDrop tempTrackDragAndDrop = self.duringTrackDragAndDrop self.duringBlockDragAndDrop = None self.duringTrackDragAndDrop = None #Now we can exit the function safely at any time without the need to reset the dragAndDrop storage in different places if tempTrackDragAndDrop: #This is only for GuiTrack aka note tracks. CC tracks follow the gui track and the conductor cannot be moved. assert not tempBlockDragAndDrop assert type(tempTrackDragAndDrop) is GuiTrack dragTrackPosition = tempTrackDragAndDrop.pos().y() trackPositions = sorted([guiTrack.pos().y() for id, guiTrack in self.tracks.items()]) trackPositions.remove(dragTrackPosition) listOfTrackIds = api.session.data.asListOfTrackIds() listOfTrackIds.remove(tempTrackDragAndDrop.staticExportItem["id"]) #Calculate the new track position and trigger a redraw canditateForNewTrackPosition = 0 #this takes care of a score with only one track and also if you move a track to the 0th position for y in trackPositions: if dragTrackPosition >= y: canditateForNewTrackPosition = trackPositions.index(y) +1 listOfTrackIds.insert(canditateForNewTrackPosition, tempTrackDragAndDrop.staticExportItem["id"]) if len(listOfTrackIds) == 1: tempTrackDragAndDrop.setPos(tempTrackDragAndDrop.staticExportItem["guiPosStart"]) #no need to trigger an api call with undo history etc. else: api.rearrangeTracks(listOfTrackIds) elif tempBlockDragAndDrop: #CC blocks, note blocks and conductor blocks assert not tempTrackDragAndDrop tempBlockDragAndDrop.mouseReleaseEventCustom(event) targetTrack = self.trackAt(event.scenePos()) #this ALWAYS returns a Conductor, GuiTrack or None. Not a CC sub-track. for CC see below when we test the block type and change this variable. targetBlock = self.blockAt(event.scenePos()) dragBlockId = tempBlockDragAndDrop.staticExportItem["id"] #First some basic checks: if not targetTrack: #Only drag and drop into tracks. return None if targetBlock is tempBlockDragAndDrop: #block got moved on itself return None #Now check what kind of block moving we are dealing with #If the drag and drop mixes different block/track types we exit blockType = type(tempBlockDragAndDrop) conductorBlock = noteBlock = ccBlock = False if blockType is GuiBlockHandle: if type(targetTrack) is GuiTrack and self.parentView.mode() in ("block", "notation"): noteBlock = True else: #Drag and Drop between different track types. return None elif blockType is ConductorTransparentBlock: if targetTrack is self.conductor: conductorBlock = True else: #Drag and Drop between different track types. return None elif blockType is CCGraphTransparentBlock: if (not type(targetTrack) is GuiTrack) or (not self.parentView.mode() == "cc"): return None ccBlock = True if constantsAndConfigs.ccViewValue in targetTrack.ccPaths: #this is only the backend database of CCs in tracks but it should be in sync. targetTrackId = targetTrack.staticExportItem["id"] #we need this later. save it before changing the targetBlock variable targetTrack = targetTrack.ccPaths[constantsAndConfigs.ccViewValue] else: return None #TODO: Create a new CC sub-track with the moved block as first block. Mainly a backend call. Needs a call at the end of this function as well. else: raise TypeError("Block must be a conductor, note or CC type but is {}".format(blockType)) #We now have a track and the track type matches the block type. #Find the position to insert. if targetBlock is None: #behind the last block positionToInsert = len(targetTrack.transparentBlockHandles) #essentially we want append() but insert() is compatible with all types of operation here. len is based 1, so len results in "after the last one" position. else: assert type(targetBlock) is blockType positionToInsert = targetTrack.transparentBlockHandles.index(targetBlock) assert positionToInsert >= 0 #Create the new order by putting the old block into a new slot and removing it from its old position newBlockOrder = [guiBlock.staticExportItem["id"] for guiBlock in targetTrack.transparentBlockHandles] if targetTrack == tempBlockDragAndDrop.parent: newBlockOrder.remove(dragBlockId) #block will be at another position and removed from its old one. newBlockOrder.insert(positionToInsert, dragBlockId) #Finally call the appropriate backend function which will trigger a GuiUpdate. if targetTrack is tempBlockDragAndDrop.parent: #Same track or within the conductor track if conductorBlock: assert targetTrack is self.conductor api.rearrangeTempoBlocks(newBlockOrder) elif noteBlock: api.rearrangeBlocks(targetTrack.staticExportItem["id"], newBlockOrder) elif ccBlock: api.rearrangeCCBlocks(targetTrackId, constantsAndConfigs.ccViewValue, newBlockOrder) #else is already catched by a Else-TypeError above else: #Different Track, if conductorBlock or targetTrack is self.conductor: raise RuntimeError("How did this slip through? Checking for cross-track incompatibility was already done above") elif noteBlock: api.moveBlockToOtherTrack(dragBlockId, targetTrack.staticExportItem["id"], newBlockOrder) elif ccBlock: #TODO: Also different CC in the same GuiTrack api.moveCCBlockToOtherTrack(dragBlockId, targetTrack.parentDataTrackId, newBlockOrder) elif self.parentView.mode() == "notation" and (not (tempBlockDragAndDrop or tempTrackDragAndDrop)) and event.button() == 1: #a positional mouse left click in a note-track, but only it not drag-drop release. track = self.trackAt(event.scenePos()) if track and not track is self.conductor: modifiers = QtWidgets.QApplication.keyboardModifiers() trackId = track.staticExportItem["id"] if modifiers == QtCore.Qt.ShiftModifier: api.selectToTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio) else: api.toTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio) elif (not event.button() == 1) and event.scenePos().x() < -25: #This is also part of musicstructures.py createInitialSignatureArea() (Track). We set a margin against misclicks. self.parentView.mainWindow.ui.actionData_Editor.trigger() super().mouseReleaseEvent(event) def reactToHorizontalScroll(self, value:int): """This was created to keep the block-mode centered track name in the center when scrolling. We forward to all tracks At point of creation the scroll value was ignored.""" viewCenterAsScenePos = self.parentView.mapToScene(self.parentView.viewport().rect().topLeft()) centerSceneX = viewCenterAsScenePos.x() + 25 #just some padding for track in self.tracks.values(): track.centerBlockModeNameGraphic(centerSceneX) #we do not need to call super. this is attached with a connect signal, not a function override.