diff --git a/engine/instrument.py b/engine/instrument.py index aafebef..84d9a44 100644 --- a/engine/instrument.py +++ b/engine/instrument.py @@ -151,9 +151,6 @@ class Instrument(object): def exportMetadata(self)->dict: """ This is the big update that sends everything to build a GUI database - - Please note that we don't add the default variant here. It is only important for the - external world to know what the current variant is. Which is handled by self.exportStatus() """ parentMetadata = self.parentLibrary.config["library"] @@ -188,6 +185,7 @@ class Instrument(object): def loadSamples(self): """ Convenience starter. Use this. + Used for loading save files as well as manual loading """ if not self.enabled: self.enable() diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py index fdaf0a2..4c7593d 100644 --- a/qtgui/designer/mainwindow.py +++ b/qtgui/designer/mainwindow.py @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.resize(1364, 977) + MainWindow.resize(1175, 651) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.centralwidget) @@ -47,35 +47,6 @@ class Ui_MainWindow(object): self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.verticalLayout_2.setSpacing(0) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.auditionerWidget = QtWidgets.QWidget(self.Instruments) - self.auditionerWidget.setObjectName("auditionerWidget") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.auditionerWidget) - self.horizontalLayout.setObjectName("horizontalLayout") - self.auditionerVolumeDial = QtWidgets.QDial(self.auditionerWidget) - self.auditionerVolumeDial.setMaximumSize(QtCore.QSize(32, 32)) - self.auditionerVolumeDial.setSizeIncrement(QtCore.QSize(3, 0)) - self.auditionerVolumeDial.setMinimum(-40) - self.auditionerVolumeDial.setMaximum(0) - self.auditionerVolumeDial.setPageStep(3) - self.auditionerVolumeDial.setProperty("value", -3) - self.auditionerVolumeDial.setWrapping(False) - self.auditionerVolumeDial.setNotchTarget(3.0) - self.auditionerVolumeDial.setNotchesVisible(True) - self.auditionerVolumeDial.setObjectName("auditionerVolumeDial") - self.horizontalLayout.addWidget(self.auditionerVolumeDial) - self.label = QtWidgets.QLabel(self.auditionerWidget) - self.label.setObjectName("label") - self.horizontalLayout.addWidget(self.label) - self.auditionerMidiInputComboBox = QtWidgets.QComboBox(self.auditionerWidget) - self.auditionerMidiInputComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) - self.auditionerMidiInputComboBox.setObjectName("auditionerMidiInputComboBox") - self.horizontalLayout.addWidget(self.auditionerMidiInputComboBox) - self.auditionerCurrentInstrument_label = QtWidgets.QLabel(self.auditionerWidget) - self.auditionerCurrentInstrument_label.setObjectName("auditionerCurrentInstrument_label") - self.horizontalLayout.addWidget(self.auditionerCurrentInstrument_label) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) - self.verticalLayout_2.addWidget(self.auditionerWidget) self.instruments_treeWidget = QtWidgets.QTreeWidget(self.Instruments) self.instruments_treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.instruments_treeWidget.setProperty("showDropIndicator", False) @@ -86,46 +57,16 @@ class Ui_MainWindow(object): self.instruments_treeWidget.setObjectName("instruments_treeWidget") self.verticalLayout_2.addWidget(self.instruments_treeWidget) self.iinstruments_tabWidget.addTab(self.Instruments, "") - self.Mixer = QtWidgets.QWidget() - self.Mixer.setObjectName("Mixer") - self.mixerVerticalLayout = QtWidgets.QVBoxLayout(self.Mixer) - self.mixerVerticalLayout.setContentsMargins(0, 0, 0, 0) - self.mixerVerticalLayout.setSpacing(0) - self.mixerVerticalLayout.setObjectName("mixerVerticalLayout") - self.mixerInstructionLabel = QtWidgets.QLabel(self.Mixer) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.mixerInstructionLabel.sizePolicy().hasHeightForWidth()) - self.mixerInstructionLabel.setSizePolicy(sizePolicy) - self.mixerInstructionLabel.setWordWrap(True) - self.mixerInstructionLabel.setObjectName("mixerInstructionLabel") - self.mixerVerticalLayout.addWidget(self.mixerInstructionLabel) - self.scrollArea = QtWidgets.QScrollArea(self.Mixer) - self.scrollArea.setWidgetResizable(True) - self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.scrollArea.setObjectName("scrollArea") - self.mixerAreaWidget = QtWidgets.QWidget() - self.mixerAreaWidget.setGeometry(QtCore.QRect(0, 0, 98, 113)) - self.mixerAreaWidget.setObjectName("mixerAreaWidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.mixerAreaWidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.progressBar = QtWidgets.QProgressBar(self.mixerAreaWidget) - self.progressBar.setProperty("value", 24) - self.progressBar.setOrientation(QtCore.Qt.Vertical) - self.progressBar.setTextDirection(QtWidgets.QProgressBar.TopToBottom) - self.progressBar.setObjectName("progressBar") - self.horizontalLayout_2.addWidget(self.progressBar) - self.progressBar_2 = QtWidgets.QProgressBar(self.mixerAreaWidget) - self.progressBar_2.setProperty("value", 24) - self.progressBar_2.setOrientation(QtCore.Qt.Vertical) - self.progressBar_2.setObjectName("progressBar_2") - self.horizontalLayout_2.addWidget(self.progressBar_2) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem1) - self.scrollArea.setWidget(self.mixerAreaWidget) - self.mixerVerticalLayout.addWidget(self.scrollArea) - self.iinstruments_tabWidget.addTab(self.Mixer, "") + self.Favorites = QtWidgets.QWidget() + self.Favorites.setObjectName("Favorites") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.Favorites) + self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_5.setSpacing(0) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.favorites_treeWidget = QtWidgets.QTreeWidget(self.Favorites) + self.favorites_treeWidget.setObjectName("favorites_treeWidget") + self.verticalLayout_5.addWidget(self.favorites_treeWidget) + self.iinstruments_tabWidget.addTab(self.Favorites, "") self.details_groupBox = QtWidgets.QGroupBox(self.splitter) self.details_groupBox.setObjectName("details_groupBox") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.details_groupBox) @@ -136,7 +77,7 @@ class Ui_MainWindow(object): self.details_scrollArea.setWidgetResizable(True) self.details_scrollArea.setObjectName("details_scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 1182, 225)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 993, 106)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.formLayout = QtWidgets.QFormLayout(self.scrollAreaWidgetContents) self.formLayout.setObjectName("formLayout") @@ -160,6 +101,35 @@ class Ui_MainWindow(object): self.details_scrollArea.setWidget(self.scrollAreaWidgetContents) self.verticalLayout_3.addWidget(self.details_scrollArea) self.verticalLayout.addWidget(self.splitter) + self.auditionerWidget = QtWidgets.QWidget(self.rightFrame) + self.auditionerWidget.setObjectName("auditionerWidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.auditionerWidget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.auditionerVolumeDial = QtWidgets.QDial(self.auditionerWidget) + self.auditionerVolumeDial.setMaximumSize(QtCore.QSize(32, 32)) + self.auditionerVolumeDial.setSizeIncrement(QtCore.QSize(3, 0)) + self.auditionerVolumeDial.setMinimum(-40) + self.auditionerVolumeDial.setMaximum(0) + self.auditionerVolumeDial.setPageStep(3) + self.auditionerVolumeDial.setProperty("value", -3) + self.auditionerVolumeDial.setWrapping(False) + self.auditionerVolumeDial.setNotchTarget(3.0) + self.auditionerVolumeDial.setNotchesVisible(True) + self.auditionerVolumeDial.setObjectName("auditionerVolumeDial") + self.horizontalLayout.addWidget(self.auditionerVolumeDial) + self.label = QtWidgets.QLabel(self.auditionerWidget) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.auditionerMidiInputComboBox = QtWidgets.QComboBox(self.auditionerWidget) + self.auditionerMidiInputComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.auditionerMidiInputComboBox.setObjectName("auditionerMidiInputComboBox") + self.horizontalLayout.addWidget(self.auditionerMidiInputComboBox) + self.auditionerCurrentInstrument_label = QtWidgets.QLabel(self.auditionerWidget) + self.auditionerCurrentInstrument_label.setObjectName("auditionerCurrentInstrument_label") + self.horizontalLayout.addWidget(self.auditionerCurrentInstrument_label) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.verticalLayout.addWidget(self.auditionerWidget) self.horizontalPianoFrame = QtWidgets.QFrame(self.rightFrame) self.horizontalPianoFrame.setMinimumSize(QtCore.QSize(0, 100)) self.horizontalPianoFrame.setFrameShape(QtWidgets.QFrame.StyledPanel) @@ -171,7 +141,7 @@ class Ui_MainWindow(object): self.horizontalLayout_3.addWidget(self.rightFrame) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1364, 20)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1175, 20)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) @@ -185,17 +155,21 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.label.setText(_translate("MainWindow", "Auditioner MIDI Input")) - self.auditionerCurrentInstrument_label.setText(_translate("MainWindow", "TextLabel")) self.instruments_treeWidget.headerItem().setText(1, _translate("MainWindow", "ID")) self.instruments_treeWidget.headerItem().setText(2, _translate("MainWindow", "Volume")) self.instruments_treeWidget.headerItem().setText(3, _translate("MainWindow", "Name")) self.instruments_treeWidget.headerItem().setText(4, _translate("MainWindow", "Variant")) self.instruments_treeWidget.headerItem().setText(5, _translate("MainWindow", "Tags")) self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Instruments), _translate("MainWindow", "Instruments")) - self.mixerInstructionLabel.setText(_translate("MainWindow", "This mixer controls the amount of each loaded instrument in the optional mixer output-ports. Idividual instrument outputs are unaffected.")) - self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Mixer), _translate("MainWindow", "Mixer")) + self.favorites_treeWidget.headerItem().setText(1, _translate("MainWindow", "ID")) + self.favorites_treeWidget.headerItem().setText(2, _translate("MainWindow", "Volume")) + self.favorites_treeWidget.headerItem().setText(3, _translate("MainWindow", "Name")) + self.favorites_treeWidget.headerItem().setText(4, _translate("MainWindow", "Variant")) + self.favorites_treeWidget.headerItem().setText(5, _translate("MainWindow", "Tags")) + self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Favorites), _translate("MainWindow", "Favorites")) self.details_groupBox.setTitle(_translate("MainWindow", "NamePlaceholder")) self.variant_label.setText(_translate("MainWindow", "Variants")) self.info_label.setText(_translate("MainWindow", "TextLabel")) self.keySwitch_label.setText(_translate("MainWindow", "KeySwitch")) + self.label.setText(_translate("MainWindow", "Auditioner MIDI Input")) + self.auditionerCurrentInstrument_label.setText(_translate("MainWindow", "TextLabel")) diff --git a/qtgui/designer/mainwindow.ui b/qtgui/designer/mainwindow.ui index 1ba06bf..facc329 100644 --- a/qtgui/designer/mainwindow.ui +++ b/qtgui/designer/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 1364 - 977 + 1175 + 651 @@ -101,83 +101,6 @@ 0 - - - - - - - - 32 - 32 - - - - - 3 - 0 - - - - -40 - - - 0 - - - 3 - - - -3 - - - false - - - 3.000000000000000 - - - true - - - - - - - Auditioner MIDI Input - - - - - - - QComboBox::AdjustToContents - - - - - - - TextLabel - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - @@ -232,11 +155,11 @@ - + - Mixer + Favorites - + 0 @@ -253,77 +176,37 @@ 0 - - - - 0 - 0 - - - - This mixer controls the amount of each loaded instrument in the optional mixer output-ports. Idividual instrument outputs are unaffected. - - - true - - - - - - - true - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - 0 - 0 - 98 - 113 - + + + + - - - - - 24 - - - Qt::Vertical - - - QProgressBar::TopToBottom - - - - - - - 24 - - - Qt::Vertical - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + + + + ID + + + + + Volume + + + + + Name + + + + + Variant + + + + + Tags + + @@ -359,8 +242,8 @@ 0 0 - 1182 - 225 + 993 + 106 @@ -405,6 +288,83 @@ + + + + + + + + 32 + 32 + + + + + 3 + 0 + + + + -40 + + + 0 + + + 3 + + + -3 + + + false + + + 3.000000000000000 + + + true + + + + + + + Auditioner MIDI Input + + + + + + + QComboBox::AdjustToContents + + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -432,7 +392,7 @@ 0 0 - 1364 + 1175 20 diff --git a/qtgui/favorites.py b/qtgui/favorites.py new file mode 100644 index 0000000..5faa921 --- /dev/null +++ b/qtgui/favorites.py @@ -0,0 +1,47 @@ +#! /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 ), + +This application 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 + +#Our Qt +from .instrument import InstrumentTreeController + + +#Engine +import engine.api as api + + +class FavoritesTreeController(InstrumentTreeController): + pass + + + +#Most Loaded +#Most Recent +#Manuelle Favoriten? + + +#Selection an den anderen weitergeben.+++++ diff --git a/qtgui/horizontalpiano.py b/qtgui/horizontalpiano.py index 154a306..d06aa78 100644 --- a/qtgui/horizontalpiano.py +++ b/qtgui/horizontalpiano.py @@ -30,6 +30,7 @@ from template.engine.duration import baseDurationToTraditionalNumber #User modules import engine.api as api +from .instrument import GuiInstrument, GuiLibrary #for the types from .verticalpiano import WIDTH as HEIGHT from .verticalpiano import STAFFLINEGAP as WIDTH WIDTH = WIDTH * 1.5 @@ -64,6 +65,27 @@ class HorizontalPiano(QtWidgets.QGraphicsView): self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + event.pixelDelta().y()) #y because it is the original vert. scroll + def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem): + """ + Program wide GUI-only callback from + widget.currentItemChanged->mainWindow.currentTreeItemChanged. We set the currentItem + ourselves, so we need to block our signals to avoid recursion. + + Only one item can be selected at a time. + + The currentTreeItem we receive is not a global instance but from a widget different to ours. + We need to find our local version of the same instrument/library/idKey first. + """ + + isLibrary = type(currentTreeItem) is GuiLibrary + idKey = currentTreeItem.idKey + + if isLibrary: + self.pianoScene.selectedInstrumentChanged(None) + else: + self.pianoScene.selectedInstrumentChanged(currentTreeItem.cachedInstrumentStatus) + + class _HorizontalPianoScene(QtWidgets.QGraphicsScene): """Most of this is copy paste from piano grid""" @@ -80,7 +102,7 @@ class _HorizontalPianoScene(QtWidgets.QGraphicsScene): self.allKeys = {} # pitch/int : BlackKey or WhiteKey self.numberLabels = [] #index is pitch - self._selectedInstrument = None #tuple instrumentStatus, instrumentData + self._selectedInstrument = None #instrumentStatus dict self._leftMouseDown = False #For note preview self.gridPen = QtGui.QPen(QtCore.Qt.SolidLine) @@ -168,7 +190,7 @@ class _HorizontalPianoScene(QtWidgets.QGraphicsScene): """GUI callback. Data is live""" #Is this for us? - if instrumentStatus and self._selectedInstrument and not instrumentStatus["idKey"] == self._selectedInstrument[0]["idKey"]: + if instrumentStatus and self._selectedInstrument and not instrumentStatus["idKey"] == self._selectedInstrument["idKey"]: return #else: # print ("not for us", instrumentStatus["idKey"]) @@ -197,7 +219,7 @@ class _HorizontalPianoScene(QtWidgets.QGraphicsScene): #self.numberLabels[keyPitch].hide() - def selectedInstrumentChanged(self, instrumentStatus, instrumentData): + def selectedInstrumentChanged(self, instrumentStatus): """GUI click to different instrument. The arguments are cached GUI data If a library is clicked, and not an instrument, both parameters will be None. @@ -207,15 +229,15 @@ class _HorizontalPianoScene(QtWidgets.QGraphicsScene): self.clearHorizontalPiano() self.fakeDeactivationOverlay.show() else: - self._selectedInstrument = (instrumentStatus, instrumentData) + self._selectedInstrument = instrumentStatus self.instrumentStatusChanged(instrumentStatus) def highlightNoteOn(self, idKey:tuple, pitch:int, velocity:int): - if self._selectedInstrument and self._selectedInstrument[0]["idKey"] == idKey: + if self._selectedInstrument and self._selectedInstrument["idKey"] == idKey: self.allKeys[pitch].highlightOn() def highlightNoteOff(self, idKey:tuple, pitch:int, velocity:int): - if self._selectedInstrument and self._selectedInstrument[0]["idKey"] == idKey: + if self._selectedInstrument and self._selectedInstrument["idKey"] == idKey: self.allKeys[pitch].highlightOff() def allHighlightsOff(self): @@ -236,7 +258,7 @@ class _HorizontalPianoScene(QtWidgets.QGraphicsScene): def _off(self): if self._selectedInstrument and not self._lastPlayPitch is None: - status, data = self._selectedInstrument + status = self._selectedInstrument libId, instrId = status["idKey"] api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch) self._lastPlayPitch = None @@ -259,7 +281,7 @@ class _HorizontalPianoScene(QtWidgets.QGraphicsScene): if self._selectedInstrument and not pitch == self._lastPlayPitch: #TODO: Play note on at a different instrument than note off? Possible? - status, data = self._selectedInstrument + status = self._selectedInstrument if not self._lastPlayPitch is None: #Force a note off that is currently playing but not under the cursor anymore diff --git a/qtgui/instrument.py b/qtgui/instrument.py index 63e0b61..08c1600 100644 --- a/qtgui/instrument.py +++ b/qtgui/instrument.py @@ -47,15 +47,15 @@ class InstrumentTreeController(object): to use. You need to add an Item to each cell. While in TreeWidget you just create one item. """ - def __init__(self, parentMainWindow): + def __init__(self, parentMainWindow, treeWidget): self.parentMainWindow = parentMainWindow - self.treeWidget = self.parentMainWindow.ui.instruments_treeWidget + self.treeWidget = treeWidget self.reset() #Includes: #self._cachedData = None #self._cachedLastInstrumentStatus = {} # instrument idKey : status Dict - #self.guiLibraries = {} # idKey : GuiLibrary + #self.guiLibraries = {} # idKey : GuiLibrary. idKey is a tuple with second value -1, which would be the instrument. #self.guiInstruments = {} # idKey : GuiInstrument @@ -71,12 +71,16 @@ class InstrumentTreeController(object): self.treeWidget.setColumnCount(len(self.headerLabels)) self.treeWidget.setHeaderLabels(self.headerLabels) self.treeWidget.setSortingEnabled(True) + self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.treeWidget.setAlternatingRowColors(True) self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.treeWidget.header().setSortIndicator(0,0) self.treeWidget.itemDoubleClicked.connect(self.itemDoubleClicked) - self.treeWidget.itemSelectionChanged.connect(self.itemSelectionChanged) + self.treeWidget.currentItemChanged.connect(self.parentMainWindow.currentTreeItemChanged) + #self.treeWidget.itemSelectionChanged.connect(self.itemSelectionChanged) #also triggers on tab change between favorits and instruments + #self.treeWidget.itemClicked.connect(self.itemSelectionChanged) #This will not activate when using the arrow keys to select self.treeWidget.itemExpanded.connect(self.itemExpandedOrCollapsed) self.treeWidget.itemCollapsed.connect(self.itemExpandedOrCollapsed) self.treeWidget.customContextMenuRequested.connect(self.contextMenu) @@ -108,7 +112,7 @@ class InstrumentTreeController(object): self._cachedData = None self._cachedLastInstrumentStatus = {} # instrument idKey : status Dict #The next two will delete all children through the garbage collector. - self.guiLibraries = {} # idKey : GuiLibrary + self.guiLibraries = {} # idKey : GuiLibrary idKey is a tuple with second value -1, which would be the instrument. self.guiInstruments = {} # idKey : GuiInstrument @@ -116,20 +120,32 @@ class InstrumentTreeController(object): if type(libraryItem) is GuiLibrary : #just in case api.session.guiSharedDataToSave["libraryIsExpanded"][libraryItem.id] = libraryItem.isExpanded() - def itemSelectionChanged(self): - """Only one instrument can be selected at the same time. - This function mostly informs other widgets that a different instrument was selected + + def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem): """ - selItems = self.treeWidget.selectedItems() - if not selItems: - return + Program wide GUI-only callback from + widget.currentItemChanged->mainWindow.currentTreeItemChanged. We set the currentItem + ourselves, so we need to block our signals to avoid recursion. - assert len(selItems) == 1, selItems #because our selection is set to single. - item = selItems[0] - if type(item) is GuiInstrument: - self.parentMainWindow.selectedInstrumentController.instrumentChanged(item.idKey) + Only one item can be selected at a time. + + The currentTreeItem we receive is not a global instance but from a widget different to ours. + We need to find our local version of the same instrument/library/idKey first. + """ + self.treeWidget.blockSignals(True) + + isLibrary = type(currentTreeItem) is GuiLibrary + idKey = currentTreeItem.idKey + + if isLibrary: + assert idKey in self.guiLibraries, (idKey, self.guiLibraries) + item = self.guiLibraries[idKey] else: - self.parentMainWindow.selectedInstrumentController.directLibrary(item.idKey) + assert idKey in self.guiInstruments, (idKey, self.guiInstruments) + item = self.guiInstruments[idKey] + + self.treeWidget.setCurrentItem(item) #This will work at first, but as soon as the tab instr/favorites is changed this will jump back to a wrong item (first of the group). We have a tabWidget.changed signal in the main window to reset this + self.treeWidget.blockSignals(False) def itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int): """This chooses the auditioner. Callbacks are handled in the auditioner widget itself""" @@ -206,7 +222,7 @@ class InstrumentTreeController(object): """ #Reset everything except our cached data. self.treeWidget.clear() #will delete the C++ objects. We need to delete the PyQt objects ourselves, like so: - self.guiLibraries = {} # idKey : GuiLibrary + self.guiLibraries = {} # idKey : GuiLibrary idKey is a tuple with second value -1, which would be the instrument. self.guiInstruments = {} # idKey : GuiInstrument if data: @@ -223,7 +239,7 @@ class InstrumentTreeController(object): if nested: parentLibraryWidget = GuiLibrary(parentTreeController=self, libraryDict=libraryDict["library"]) - self.guiLibraries[libraryId] = parentLibraryWidget + self.guiLibraries[(libraryId, -1)] = parentLibraryWidget #-1 marks the library but keeps it a tuple. self.treeWidget.addTopLevelItem(parentLibraryWidget) if libraryId in api.session.guiSharedDataToSave["libraryIsExpanded"]: parentLibraryWidget.setExpanded(api.session.guiSharedDataToSave["libraryIsExpanded"][libraryId]) #only possible after gi.init() was done and item inserted. @@ -270,7 +286,7 @@ class InstrumentTreeController(object): """We do not use the qt function collapseAll and expandAll because they do not trigger the signal""" if self.isNested(): - for libid, guiLib in self.guiLibraries.items(): + for idKey, guiLib in self.guiLibraries.items(): guiLib.setExpanded(state) def _adjustColumnSize(self): @@ -334,7 +350,7 @@ class GuiLibrary(QtWidgets.QTreeWidgetItem): super().__init__([], type=1000) #type 0 is default qt type. 1000 is subclassed user type) self.id = libraryDict["id"] - self.idKey = (libraryDict["id"], 0) #fake it for compatibility + self.idKey = (libraryDict["id"], -1) #fake it for compatibility. -1 means library self.name = libraryDict["name"] @@ -474,15 +490,22 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem): def instrumentSwitchOnViaGui(self, state): """Only GUI clicks. Does not react to the engine callback that switches on instruments. For - example one that arrives through "load group" or "load all" """ + example one that arrives through "load group" or "load all" + + We use this to count, across program runs, how many times this instrument was activated + and present it as favourites. The engine does not know about this. + Loading states from save files, "load all" and other algorithmic loaders do not contribute + to this counting. + """ if state: api.loadInstrumentSamples(self.idKey) + self.parentTreeController.parentMainWindow.favoriteInstrument(self.idKey) else: api.unloadInstrumentSamples(self.idKey) def updateStatus(self, instrumentStatus:dict): #Before we set the state permanently we use the opportunity to see if this is program state (state == None) or unloading - self._cachedInstrumentStatus = instrumentStatus + self.cachedInstrumentStatus = instrumentStatus firstLoad = self.state is None variantColumnIndex = self.columns.index("loaded") self.currentVariant = instrumentStatus["currentVariant"] @@ -521,7 +544,7 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem): def _mixSendDialContextMenuEvent(self, event): - if self._cachedInstrumentStatus["mixerEnabled"]: + if self.cachedInstrumentStatus["mixerEnabled"]: mixerMuteText = QtCore.QCoreApplication.translate("InstrumentMixerLevelContextMenu", "Mute/Disable Mixer-Send for {}".format(self.instrumentDict["name"])) mixerMuteFunc = lambda: api.setInstrumentMixerEnabled(self.idKey, False) else: diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index 58c16ec..9f919ac 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -23,6 +23,7 @@ import logging; logging.info("import {}".format(__file__)) #Standard Library Modules import pathlib import os +from collections import Counter #Third Party Modules from PyQt5 import QtWidgets, QtCore, QtGui @@ -37,6 +38,7 @@ import engine.api as api from engine.config import * #imports METADATA from .instrument import InstrumentTreeController +from .favorites import FavoritesTreeController from .auditioner import AuditionerMidiInputComboController from .selectedinstrumentcontroller import SelectedInstrumentController from .verticalpiano import VerticalPiano @@ -68,18 +70,27 @@ class MainWindow(TemplateMainWindow): super().__init__() + #To prevent double counting of instruments loads we remember what we already loaded this time. The permanent database is in the qt settings. + self.favoriteInstrumentsThisRun = set() #idKey Tuple + #Make the description field at least a bit visible self.ui.details_groupBox.setMinimumSize(1, 50) self.ui.splitter.setSizes([1,1]) #just a forced update self.auditionerMidiInputComboController = AuditionerMidiInputComboController(parentMainWindow=self) - self.instrumentTreeController = InstrumentTreeController(parentMainWindow=self) + self.instrumentTreeController = InstrumentTreeController(parentMainWindow=self, treeWidget=self.ui.instruments_treeWidget) + self.favoritesTreeController = FavoritesTreeController(parentMainWindow=self, treeWidget=self.ui.favorites_treeWidget) self.selectedInstrumentController = SelectedInstrumentController(parentMainWindow=self) - self.tabWidget = self.ui.iinstruments_tabWidget #with this ugly name it is only a matter of time that it gets changed + self.tabWidget = self.ui.iinstruments_tabWidget #with this ugly name it is only a matter of time that it gets changed. Better save in a permanent var. self.tabWidget.setTabBarAutoHide(True) - self.tabWidget.setTabVisible(1, False) #Hide Mixer until we decide if we need it. #TODO - + #self.tabWidget.setTabVisible(2, False) #Hide Favorites until we decide if we need it. + #The tab widget / treewidget combination triggers a bug in Qt. The treewidget will actively switch to the wrongly selected item. + #We added a signal on tabChanged to set it back as a hack/workaround. Remember the real item here: + self.rememberCurrentItem = None #idKey + self.duringTabChange = False #our own "block signal" + self.tabWidget.tabBarClicked.connect(self.signalBlockTabAboutToChange) + self.tabWidget.currentChanged.connect(self.bugWorkaroundRestoreCurrentItem) #Set up the two pianos self.verticalPiano = VerticalPiano(self) @@ -180,7 +191,6 @@ class MainWindow(TemplateMainWindow): self.menu.addMenuEntry("menuView", "actionPianoRollVisible", QtCore.QCoreApplication.translate("Menu", "Piano Roll"), self.pianoRollToggleVisibleAndRemember, shortcut="Ctrl+R", checkable=True, startChecked=True) #function receives check state as automatic parameter self.menu.addMenuEntry("menuView", "actionPianoVisible", QtCore.QCoreApplication.translate("Menu", "Piano"), self.pianoToggleVisibleAndRemember, shortcut="Ctrl+P", checkable=True, startChecked=True) #function receives check state as automatic parameter - self.menu.orderSubmenus(["menuFile", "menuEdit", "menuView", "menuHelp"]) def react_rescanSampleDir(self): @@ -227,18 +237,82 @@ class MainWindow(TemplateMainWindow): settings.setValue("pianoVisible", state) - def selectedInstrumentChanged(self, instrumentStatus, instrumentData): - """We receive this from selectedinstrumentcontroller.py, when the user clicks on a GUI - entry for a different instrument. This is purely a GUI function. This functions - relays the change to other widgets, except the above mentioned controller. + def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem, previousTreeItem:QtWidgets.QTreeWidgetItem): + """Somewhere in the program the user-selected tree item changed. + It can be an instrument or a library. Send this change to all widgets + that deal with instruments and libraries, which are most of them. - If a library is clicked, and not an instrument, both parameters will be None. - The pianos use this to switch off. + The widgets handle the type of item themselves. We attached all engine data + to the items themselves. The items are not universal for the whole program but children + of one specific widget. The receivers of this callback need to extract their information + from the treeItem. + + Technically this is the signal QTreeWidget.currentTreeItemChanged so we receive their + parameters. """ - self.verticalPiano.pianoScene.selectedInstrumentChanged(instrumentStatus, instrumentData) - self.horizontalPiano.pianoScene.selectedInstrumentChanged(instrumentStatus, instrumentData) + if self.duringTabChange: + currentTreeItem = self.rememberCurrentItem #There is a bug in tabChange signal. The TreeWidget will send an old item + + self.rememberCurrentItem = currentTreeItem + + if not currentTreeItem: + return + + widgets = ( + self.verticalPiano, + self.horizontalPiano, + self.selectedInstrumentController, + self.instrumentTreeController, + self.favoritesTreeController, + ) + + for widget in widgets: + widget.currentTreeItemChanged(currentTreeItem) + + def signalBlockTabAboutToChange(self, tabIndex:int): + """Click on a tab. + Finalized through bugWorkaroundRestoreCurrentItem""" + self.duringTabChange = True + + def bugWorkaroundRestoreCurrentItem(self): + """The tab already has changed. + Preceded by signalBlockTabAboutToChange""" + self.duringTabChange = False def zoom(self, scaleFactor:float): pass def stretchXCoordinates(self, factor): pass + + def favoriteInstrument(self, idKey:tuple): + """Count the user-instructed, explicit loading of an instrument in the permanent database. + We work directly with the qt settings without local data. A crash or sigkill will not + prevent the counting.""" + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("favoriteInstruments"): + database = settings.value("favoriteInstruments", type=dict) + else: + database = {} + + if not idKey in self.favoriteInstrumentsThisRun: + if not idKey in database: + database[idKey] = 0 + database[idKey] += 1 + self.favoriteInstrumentsThisRun.add(idKey) + + settings.setValue("favoriteInstruments", database) + + def resetFavoriteInstrumentDatabase(self): + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("favoriteInstruments"): + settings.remove("favoriteInstruments") + self.favoriteInstrumentsThisRun = set() + + def getTop10FavoriteInstruments(self)->list: + """Return an ordered list of double tuple ((libid, instid), counter) """ + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + if settings.contains("favoriteInstruments"): + database = settings.value("favoriteInstruments", type=dict) + return Counter(database).most_common(10) #python already knows how to create that + else: + return [] diff --git a/qtgui/selectedinstrumentcontroller.py b/qtgui/selectedinstrumentcontroller.py index b20ad9f..1fe32f7 100644 --- a/qtgui/selectedinstrumentcontroller.py +++ b/qtgui/selectedinstrumentcontroller.py @@ -27,6 +27,7 @@ import logging; logger = logging.getLogger(__name__); logger.info("import") from PyQt5 import QtCore, QtGui, QtWidgets #Our Qt +from .instrument import GuiInstrument, GuiLibrary #for the types #Engine import engine.api as api @@ -60,9 +61,9 @@ class SelectedInstrumentController(object): api.callbacks.instrumentListMetadata.append(self.react_initialInstrumentList) api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged) - def directLibrary(self, idkey:tuple): + def directLibrary(self, idKey:tuple): """User clicked on a library treeItem""" - libraryId, instrumentId = idkey + libraryId, instrumentId = idKey self.currentIdKey = None self.ui.details_scrollArea.show() self.ui.variants_comboBox.hide() @@ -77,10 +78,6 @@ class SelectedInstrumentController(object): self.ui.info_label.setText(self._metadataToDescriptionLabel(metadata)) - #Inform other widgets (but not recursively ourselves) that we clicked on no instrument. - #e.g. to switch off the gui keyboards. - self.parentMainWindow.selectedInstrumentChanged(None, None) - def _metadataToDescriptionLabel(self, metadata:dict)->str: """Can work with instruments and libraries alike""" @@ -132,7 +129,26 @@ class SelectedInstrumentController(object): self.ui.keySwitch_comboBox.setCurrentIndex(curIdx) - def instrumentChanged(self, idkey:tuple): + def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem): + """ + Program wide GUI-only callback from + widget.currentItemChanged->mainWindow.currentTreeItemChanged. We set the currentItem + ourselves, so we need to block our signals to avoid recursion. + + Only one item can be selected at a time. + + The currentTreeItem we receive is not a global instance but from a widget different to ours. + We need to find our local version of the same instrument/library/idKey first. + """ + isLibrary = type(currentTreeItem) is GuiLibrary + idKey = currentTreeItem.idKey + + if isLibrary: + self.directLibrary(idKey) + else: + self.instrumentChanged(idKey) + + def instrumentChanged(self, idKey:tuple): """This is a GUI-internal function. The user selected a different instrument from the list. Single click, arrow keys etc. @@ -142,8 +158,8 @@ class SelectedInstrumentController(object): We also relay this information to the main window which can send it to other widgets, like the two keyboards. We will not receive this callback from the mainwindow. """ - libraryId, instrumentId = idkey - self.currentIdKey = idkey + libraryId, instrumentId = idKey + self.currentIdKey = idKey self.ui.details_scrollArea.show() @@ -168,9 +184,6 @@ class SelectedInstrumentController(object): #Dynamic self.react_instrumentStatusChanged(self.statusUpdates[libraryId][instrumentId]) - #Relay to Main Window. We will not receive this ourselves. - self.parentMainWindow.selectedInstrumentChanged(instrumentStatus, instrumentData) - def react_instrumentStatusChanged(self, instrumentStatus:dict): """Callback from the api. Has nothing to do with any GUI state or selection. @@ -194,14 +207,14 @@ class SelectedInstrumentController(object): This callback is called again by the GUI directly when switching the instrument with a mouseclick in instrumentChanged and the GUI will use the cached data. """ - idkey = instrumentStatus["idKey"] - libraryId, instrumentId = idkey + idKey = instrumentStatus["idKey"] + libraryId, instrumentId = idKey if not libraryId in self.statusUpdates: self.statusUpdates[libraryId] = {} #empty library. status dict self.statusUpdates[libraryId][instrumentId] = instrumentStatus #create or overwrite / keep up to date - if not self.currentIdKey == idkey: + if not self.currentIdKey == idKey: #Callback for an instrument currently not selected return diff --git a/qtgui/verticalpiano.py b/qtgui/verticalpiano.py index 29aa6ac..64f3f5a 100644 --- a/qtgui/verticalpiano.py +++ b/qtgui/verticalpiano.py @@ -30,6 +30,7 @@ from template.engine.duration import baseDurationToTraditionalNumber #User modules import engine.api as api +from .instrument import GuiInstrument, GuiLibrary #for the types WIDTH = 200 @@ -59,7 +60,25 @@ class VerticalPiano(QtWidgets.QGraphicsView): self.centerOn(0, 64*STAFFLINEGAP) + def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem): + """ + Program wide GUI-only callback from + widget.currentItemChanged->mainWindow.currentTreeItemChanged. We set the currentItem + ourselves, so we need to block our signals to avoid recursion. + + Only one item can be selected at a time. + + The currentTreeItem we receive is not a global instance but from a widget different to ours. + We need to find our local version of the same instrument/library/idKey first. + """ + + isLibrary = type(currentTreeItem) is GuiLibrary + idKey = currentTreeItem.idKey + if isLibrary: + self.pianoScene.selectedInstrumentChanged(None) + else: + self.pianoScene.selectedInstrumentChanged(currentTreeItem.cachedInstrumentStatus) class _VerticalPianoScene(QtWidgets.QGraphicsScene): """Most of this is copy paste from piano grid""" @@ -81,7 +100,7 @@ class _VerticalPianoScene(QtWidgets.QGraphicsScene): self.numberLabels = [] #index is pitch - self._selectedInstrument = None #tuple instrumentStatus, instrumentData + self._selectedInstrument = None #instrumentStatus dict self._leftMouseDown = False #For note preview self.gridPen = QtGui.QPen(QtCore.Qt.SolidLine) @@ -177,7 +196,7 @@ class _VerticalPianoScene(QtWidgets.QGraphicsScene): """GUI callback. Data is live""" #Is this for us? - if instrumentStatus and self._selectedInstrument and not instrumentStatus["idKey"] == self._selectedInstrument[0]["idKey"]: + if instrumentStatus and self._selectedInstrument and not instrumentStatus["idKey"] == self._selectedInstrument["idKey"]: return #else: # print ("not for us", instrumentStatus["idKey"]) @@ -205,7 +224,7 @@ class _VerticalPianoScene(QtWidgets.QGraphicsScene): #keyObject.hide() keyObject.setPlayable(keyPitch in instrumentStatus["playableKeys"]) - def selectedInstrumentChanged(self, instrumentStatus, instrumentData): + def selectedInstrumentChanged(self, instrumentStatus): """GUI click to different instrument. The arguments are cached GUI data If a library is clicked, and not an instrument, both parameters will be None. @@ -215,16 +234,16 @@ class _VerticalPianoScene(QtWidgets.QGraphicsScene): self.clearVerticalPiano() self.fakeDeactivationOverlay.show() else: - self._selectedInstrument = (instrumentStatus, instrumentData) + self._selectedInstrument = instrumentStatus self.instrumentStatusChanged(instrumentStatus) def highlightNoteOn(self, idKey:tuple, pitch:int, velocity:int): - if self._selectedInstrument and self._selectedInstrument[0]["idKey"] == idKey: + if self._selectedInstrument and self._selectedInstrument["idKey"] == idKey: highlight = self.highlights[pitch] highlight.show() def highlightNoteOff(self, idKey:tuple, pitch:int, velocity:int): - if self._selectedInstrument and self._selectedInstrument[0]["idKey"] == idKey: + if self._selectedInstrument and self._selectedInstrument["idKey"] == idKey: highlight = self.highlights[pitch] highlight.hide() @@ -245,7 +264,7 @@ class _VerticalPianoScene(QtWidgets.QGraphicsScene): def _off(self): if self._selectedInstrument and not self._lastPlayPitch is None: - status, data = self._selectedInstrument + status = self._selectedInstrument libId, instrId = status["idKey"] api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch) self._lastPlayPitch = None @@ -259,7 +278,7 @@ class _VerticalPianoScene(QtWidgets.QGraphicsScene): if self._selectedInstrument and not pitch == self._lastPlayPitch: #TODO: Play note on at a different instrument than note off? Possible? - status, data = self._selectedInstrument + status = self._selectedInstrument if not self._lastPlayPitch is None: #Force a note off that is currently playing but not under the cursor anymore