From 3fe44c91192369776656966e4c15d140f953ced2 Mon Sep 17 00:00:00 2001 From: Nils Date: Sat, 4 Jun 2022 00:16:36 +0200 Subject: [PATCH] Add new view that shows the current track as list of text items for easier navigation and overview --- engine/api.py | 5 ++ engine/cursor.py | 1 + engine/items.py | 41 ++++++++++--- engine/track.py | 7 ++- qtgui/designer/mainwindow.py | 3 + qtgui/designer/mainwindow.ui | 4 +- qtgui/mainwindow.py | 25 ++++++-- qtgui/scoreview.py | 6 +- qtgui/tracklistwidget.py | 111 +++++++++++++++++++++++++++++++++++ 9 files changed, 186 insertions(+), 17 deletions(-) create mode 100644 qtgui/tracklistwidget.py 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