diff --git a/engine/api.py b/engine/api.py
index 2de66bb..28ef115 100644
--- a/engine/api.py
+++ b/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"""
diff --git a/engine/cursor.py b/engine/cursor.py
index f523e65..1885820 100644
--- a/engine/cursor.py
+++ b/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
diff --git a/engine/items.py b/engine/items.py
index 6e845c2..7035882 100644
--- a/engine/items.py
+++ b/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.
}
diff --git a/engine/track.py b/engine/track.py
index 322ec65..8155507 100644
--- a/engine/track.py
+++ b/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():
diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py
index 210512a..06d6020 100644
--- a/qtgui/designer/mainwindow.py
+++ b/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)
diff --git a/qtgui/designer/mainwindow.ui b/qtgui/designer/mainwindow.ui
index c33a9eb..05b589d 100644
--- a/qtgui/designer/mainwindow.ui
+++ b/qtgui/designer/mainwindow.ui
@@ -16,7 +16,9 @@
-
-
+
+
+
diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py
index ef05471..1ac7270 100644
--- a/qtgui/mainwindow.py
+++ b/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()
diff --git a/qtgui/scoreview.py b/qtgui/scoreview.py
index 959549a..02ee512 100644
--- a/qtgui/scoreview.py
+++ b/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)
diff --git a/qtgui/tracklistwidget.py b/qtgui/tracklistwidget.py
new file mode 100644
index 0000000..3421daa
--- /dev/null
+++ b/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 .
+"""
+
+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