Browse Source

Add new view that shows the current track as list of text items for easier navigation and overview

master
Nils 4 months ago
parent
commit
3fe44c9119
  1. 5
      engine/api.py
  2. 1
      engine/cursor.py
  3. 41
      engine/items.py
  4. 7
      engine/track.py
  5. 3
      qtgui/designer/mainwindow.py
  6. 4
      qtgui/designer/mainwindow.ui
  7. 25
      qtgui/mainwindow.py
  8. 6
      qtgui/scoreview.py
  9. 111
      qtgui/tracklistwidget.py

5
engine/api.py

@ -1070,6 +1070,11 @@ def getCursorSimpleLyNote():
return lyNote
def toPosition(position:int):
"""Move the cursor to this position in the current track. """
session.data.currentTrack().toPosition(position=position, strict=True)
callbacks._setCursor()
def left():
"""move the currently active tracks cursor one position to the left.
Can be directly used by a user interface"""

1
engine/cursor.py

@ -124,6 +124,7 @@ class Cursor:
"type": "Cursor",
"trackIndex": trackState.index(),
"track" : trackState.track,
"trackId" : id(trackState.track),
"trackName" : trackState.track.name,
"cboxMidiOutUuid" : trackState.track.sequencerInterface.cboxMidiOutUuid, #used for midi throught. Step midi shall produce sound through the current track.
"midiChannel" : trackState.midiChannel(), #zero based

41
engine/items.py

@ -1729,6 +1729,7 @@ class Chord(Item):
"midiBytes" : midiBytesList,
"midiChannelOffset" : self.midiChannelOffset,
"beam" : tuple(), #decided later in track export. Has the same structure as a stem.
"UIstring" : self.lilypond(carryLilypondRanges={}),
}
def _lilypond(self, carryLilypondRanges):
@ -1916,7 +1917,7 @@ class Rest(Item):
"stem" : (0,0,0),
"lowestPitchAsDotOnLine" : 0,
"highestPitchAsDotOnLine" : 0,
"UIstring" : self.lilypond(carryLilypondRanges={}),
}
def _lilypond(self, carryLilypondRanges):
@ -1995,6 +1996,7 @@ class MultiMeasureRest(Item):
"tickindex" : trackState.tickindex - dur, #because we parse the tickindex after we stepped over this item.
"midiBytes" : [],
"lilypondParameters" : self.lilypondParameters,
"UIstring" : self.lilypond(carryLilypondRanges={}),
}
def _setNumberOfMeasures(self, numberOfMeasures):
@ -2217,7 +2219,7 @@ class KeySignature(Item):
"root" : self.root,
"accidentalsOnLines" : self.asAccidentalsOnLines(trackState.clef()),
"midiBytes" : [],
"UIstring" : self.lilypond(carryLilypondRanges={}),
}
def _lilypond(self, carryLilypondRanges):
@ -2354,7 +2356,7 @@ class Clef(Item):
"tickindex" : trackState.tickindex,
"clef" : self.clefString,
"midiBytes" : [],
"UIstring" : self.lilypond(carryLilypondRanges={}),
#Thats it. A GUI does not need to know anything about the clef except its look because we already deliever note pitches as dots on lines, calculated with the clef.
}
@ -2417,7 +2419,7 @@ class TimeSignature(Item): #Deprecated since 1750
"nominator" : self.nominator,
"denominator" : duration.baseDurationToTraditionalNumber[self.denominator],
"midiBytes" : [],
"UIstring" : self.lilypond(carryLilypondRanges={}),
}
class MetricalInstruction(Item):
@ -2469,6 +2471,15 @@ class MetricalInstruction(Item):
new = MetricalInstruction(self.treeOfInstructions, self.isMetrical)
return new
def asText(self):
if self.lilypondParameters["override"] == "\\mark \"X\" \\cadenzaOn":
return "\\time X"
elif self.lilypondParameters["override"].startswith("\\cadenzaOff"):
return self.lilypondParameters["override"][12:]
else:
return "MetricalInstruction"
def _exportObject(self, trackState):
return {
"type": "MetricalInstruction",
@ -2479,6 +2490,7 @@ class MetricalInstruction(Item):
"oneMeasureInTicks" : self.oneMeasureInTicks,
"midiBytes" : [],
"treeOfInstructions" : self.treeOfInstructions.__repr__(),
"UIstring" : self.asText(),
}
def _lilypond(self, carryLilypondRanges):
@ -2492,17 +2504,18 @@ class BlockEndMarker(Item):
only during track. static representation"""
def __init__(self):
super(BlockEndMarker, self).__init__()
super().__init__()
#does not need a serialize or copy method since it gets dynamically created only for export
def _exportObject(self, trackState):
return {
"type": "BlockEndMarker",
"completeDuration" : 0,
"tickindex" : trackState.tickindex,
"midiBytes" : [],
"UIstring" : f"== {trackState.blockName()}",
}
class DynamicSignature(Item):
@ -2546,6 +2559,7 @@ class DynamicSignature(Item):
"tickindex" : trackState.tickindex,
"keyword" : self.keyword,
"midiBytes" : [],
"UIstring" : self.keyword,
}
class DynamicRamp(Item):
@ -2655,7 +2669,7 @@ class DynamicRamp(Item):
"completeDuration" : 0,
"tickindex" : trackState.tickindex,
"midiBytes" : [],
"UIstring" : keyword,
}
class LegatoSlur(Item):
@ -2693,6 +2707,14 @@ class LegatoSlur(Item):
else:
return "close"
def asText(self, trackState):
if trackState.duringLegatoSlur:
return "("
else:
return ")"
def _exportObject(self, trackState):
return {
"type" : "LegatoSlur",
@ -2700,6 +2722,7 @@ class LegatoSlur(Item):
"completeDuration" : 0,
"tickindex" : trackState.tickindex,
"midiBytes" : [],
"UIstring" : self.asText(trackState),
}
class InstrumentChange(Item):
@ -2965,7 +2988,7 @@ class LilypondText(Item):
"type" : "LilypondText",
"completeDuration" : 0,
"tickindex" : trackState.tickindex, #we parse the tickindex after we stepped over the item.
"midiBytes" : [],
"midiBytes" : [],
"text" : self.text,
"UIstring" : self.text, #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible.
"UIstring" : self.text, #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible.
}

7
engine/track.py

@ -319,6 +319,9 @@ class TrackState(object):
else:
return None #hidden
def blockName(self):
return self.track.currentBlock().name
def clef(self):
return self.clefs[-1]
@ -640,7 +643,9 @@ class Track(object):
return True
def toPosition(self, position, strict = True):
"""wants track.state.position() as parameter"""
"""wants track.state.position() as parameter.
This includes the auto-generated block boundaries, which makes this function easy.
"""
self.head()
while not self.state.position() == position:
if not self.right():

3
qtgui/designer/mainwindow.py

@ -22,6 +22,9 @@ class Ui_MainWindow(object):
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.mainStackWidget = QtWidgets.QStackedWidget(self.centralwidget)
self.mainStackWidget.setObjectName("mainStackWidget")
self.scoreViewParentContainer = QtWidgets.QWidget()
self.scoreViewParentContainer.setObjectName("scoreViewParentContainer")
self.mainStackWidget.addWidget(self.scoreViewParentContainer)
self.verticalLayout_2.addWidget(self.mainStackWidget)
MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(MainWindow)

4
qtgui/designer/mainwindow.ui

@ -16,7 +16,9 @@
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QStackedWidget" name="mainStackWidget"/>
<widget class="QStackedWidget" name="mainStackWidget">
<widget class="QWidget" name="scoreViewParentContainer"/>
</widget>
</item>
</layout>
</widget>

25
qtgui/mainwindow.py

@ -42,9 +42,12 @@ from .menu import MenuActionDatabase
from .submenus import BlockPropertiesEdit
from .scoreview import ScoreView
from .trackEditor import TrackEditor
from .tracklistwidget import TrackListWidget
from .resources import *
from engine.config import METADATA
MAX_QT_SIZE = 2147483647-1
class MainWindow(TemplateMainWindow):
def __init__(self):
@ -90,9 +93,22 @@ class MainWindow(TemplateMainWindow):
#Create the Main Widgets in the Stacked Widget
self.scoreView = ScoreView(self)
self.ui.mainStackWidget.addWidget(self.scoreView)
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.scoreView))
horizontalLayout = QtWidgets.QHBoxLayout(self.ui.scoreViewParentContainer)
self.scoreSplitter = QtWidgets.QSplitter(self.ui.scoreViewParentContainer)
self.scoreSplitter.setOrientation(QtCore.Qt.Horizontal)
self.scoreView = ScoreView(mainWindow=self, parentSplitter=self.scoreSplitter)
#self.ui.mainStackWidget.addWidget(self.scoreView)
self.trackListWidget = TrackListWidget(mainWindow=self, parentSplitter=self.scoreSplitter)
horizontalLayout.addWidget(self.scoreSplitter)
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.ui.scoreViewParentContainer))
#Set the splitter ratio.
self.scoreView.setMinimumSize(1, 1)
self.trackListWidget.setMinimumSize(1, 1)
self.scoreSplitter.setSizes([16, 1])
#self.scoreSplitter.setSizes([MAX_QT_SIZE, 1])
self.trackEditor = QtWidgets.QScrollArea()
self.trackEditor.setWidgetResizable(True)
@ -129,6 +145,7 @@ class MainWindow(TemplateMainWindow):
self.ui.actionAutoconnect_Metronome.setChecked(autoconnectMixer)
self.reactToAutoconnectMixerCheckbox(autoconnectMixer) #do the connection
else:
self.ui.actionAutoconnect_Metronome.setEnabled(False)
@ -189,7 +206,7 @@ class MainWindow(TemplateMainWindow):
self.menuActionDatabase.writeProtection(True)
self.trackEditor.setEnabled(True)
else:
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.scoreView))
self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.ui.scoreViewParentContainer))
self.scoreView.setEnabled(True)
self.menuActionDatabase.writeProtection(False)
self.scoreView.updateMode()

6
qtgui/scoreview.py

@ -35,8 +35,10 @@ from .submenus import GridRhytmEdit
import engine.api as api
class ScoreView(QtWidgets.QGraphicsView):
def __init__(self, mainWindow):
super().__init__()
def __init__(self, mainWindow, parentSplitter):
super().__init__(parentSplitter)
self.mainWindow = mainWindow
self.scoreScene = GuiScore(parentView=self)

111
qtgui/tracklistwidget.py

@ -0,0 +1,111 @@
#! /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 <http://www.gnu.org/licenses/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
#Template Modules
#Our Modules
import engine.api as api
class TrackListWidget(QtWidgets.QListWidget):
"""The TrackListWidget holds all tracks as text-only variants with cursor access.
It will show one track at a time, the current one.
It is meant as alternative access for hard-to-reach items, such as zero-duration-items.
The cursor decides which track is currently handled as items.
But track content changes can come it for any track at any time.
In opposite to the ScoreScene we do not deal with deleted and hidden tracks.
We recreate the items on track change.
"""
def __init__(self, mainWindow, parentSplitter):
super().__init__(parentSplitter)
self.mainWindow = mainWindow
self._lastCurrentTrack = None
self.tracks = {} # engineTrackId : engineExportData
self.itemPressed.connect(self._react_itemPressed)
api.callbacks.tracksChanged.append(self.syncTracks)
api.callbacks.updateTrack.append(self.updateTrack)
api.callbacks.setCursor.append(self.setCursor)
def setCursor(self, cursorExportObject):
if not self._lastCurrentTrack == cursorExportObject["trackId"]:
self._lastCurrentTrack == cursorExportObject["trackId"]
self.showTrack(cursorExportObject["trackId"])
self.blockSignals(True)
self.setCurrentRow(cursorExportObject["position"])
self.blockSignals(False)
def _react_itemPressed(self, item):
"""This is gui->api cursor setting
It only happens when the mouse etc. actually pressed an item.
This list cannot be activated by cursor keys etc. directly. """
api.toPosition(self.currentRow()) #currentRow is calculated after we already are on the item.
def syncTracks(self, listOfStaticTrackRepresentations):
"""Handles the number of tracks and track meta-data changes,
but not track contents, which is handled by self.updateTrack through a different callback"""
for trackExportObject in listOfStaticTrackRepresentations:
if not trackExportObject["id"] in self.tracks:
self.tracks[trackExportObject["id"]] = None
def updateTrack(self, trackId, staticRepresentationList):
"""Callback: The content of a single track has changed"""
if not trackId in self.tracks:
#hidden track. But this can still happen through the data editor
return
self.tracks[trackId] = staticRepresentationList
if trackId == self._lastCurrentTrack:
self.showTrack(trackId)
def showTrack(self, trackId):
"""Cursor moved to a different track.
This is essentially destroy-and-recreate in Qt """
self.clear()
staticRepresentationList = self.tracks[trackId]
count = 0
for staticItem in staticRepresentationList:
self.addItem(staticItem["UIstring"])
self.addItem("|") #Appending
Loading…
Cancel
Save