Browse Source

Show control change labels and make them gui midi controls. Expand default instrument to show off the feature

master
Nils 2 years ago
parent
commit
b72bd48ba2
  1. 2
      README.md
  2. 2
      documentation/out/english.html
  3. 2
      documentation/out/german.html
  4. 27
      engine/api.py
  5. 7
      engine/instrument.py
  6. 3
      engine/main.py
  7. BIN
      engine/resources/000 - Default.tembro
  8. 66
      qtgui/designer/mainwindow.py
  9. 82
      qtgui/designer/mainwindow.ui
  10. 102
      qtgui/selectedinstrumentcontroller.py
  11. 159
      template/qtgui/flowlayout.py

2
README.md

@ -1,5 +1,5 @@
[//]: # (Generated 2022-04-08T17:30:22.829359. Changes belong into template/documentation/readme.template)
[//]: # (Generated 2022-04-08T17:43:00.491140. Changes belong into template/documentation/readme.template)
# Tembro

2
documentation/out/english.html

@ -717,7 +717,7 @@ The program is split in two parts. A shared "template" between the Laborejo Soft
</div>
<div id="footer">
<div id="footer-text">
Last updated 2022-04-08 17:30:22 +0200
Last updated 2022-04-08 17:43:00 +0200
</div>
</div>
</body>

2
documentation/out/german.html

@ -711,7 +711,7 @@ Ansonsten starten Sie tembro mit diesem Befehl, Sprachcode ändern, vom Terminal
</div>
<div id="footer">
<div id="footer-text">
Last updated 2022-04-08 17:30:22 +0200
Last updated 2022-04-08 17:43:00 +0200
</div>
</div>
</body>

27
engine/api.py

@ -51,6 +51,7 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
self.auditionerVolumeChanged = []
self.instrumentMidiNoteOnActivity = []
self.instrumentMidiNoteOffActivity = []
self.instrumentCCActivity = []
def _tempCallback(self):
"""Just for copy paste during development"""
@ -133,6 +134,10 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
for func in self.instrumentMidiNoteOffActivity:
func(idKey, pitch, velocity)
def _instrumentCCActivity(self, idKey, ccNumber, value):
for func in self.instrumentCCActivity:
func(idKey, ccNumber, value)
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
from template.engine.api import callbacks
@ -140,7 +145,7 @@ from template.engine.api import callbacks
_templateStartEngine = startEngine
def startEngine(nsmClient, additionalData):
session.data.parseAndLoadInstrumentLibraries(additionalData["baseSamplePath"], callbacks._instrumentMidiNoteOnActivity, callbacks._instrumentMidiNoteOffActivity )
session.data.parseAndLoadInstrumentLibraries(additionalData["baseSamplePath"], callbacks._instrumentMidiNoteOnActivity, callbacks._instrumentMidiNoteOffActivity, callbacks._instrumentCCActivity )
_templateStartEngine(nsmClient) #loads save files or creates empty structure.
@ -193,7 +198,7 @@ def rescanSampleDirectory(newBaseSamplePath):
#In Tembro instruments never musically change through updates.
#There is no musical danger of keeping an old version alive, even if a newly downloaded .tar
#contains updates. A user must load/reload these manually or restart the program.
session.data.parseAndLoadInstrumentLibraries(newBaseSamplePath, session.data.instrumentMidiNoteOnActivity, session.data.instrumentMidiNoteOffActivity)
session.data.parseAndLoadInstrumentLibraries(newBaseSamplePath, session.data.instrumentMidiNoteOnActivity, session.data.instrumentMidiNoteOffActivity, session.data.instrumentCCActivity )
callbacks._rescanSampleDir() #instructs the GUI to forget all cached data and start fresh.
callbacks._instrumentListMetadata() #The big "build the database" callback
@ -364,3 +369,21 @@ def sendNoteOffToInstrument(idKey:tuple, midipitch:int):
if instrument.enabled:
instrument.scene.send_midi_event(0x80, midipitch, 0)
callbacks._instrumentMidiNoteOffActivity(idKey, midipitch, 0)
def ccTrackingState(idKey:tuple):
"""Get the current values of all CCs that have been modified through midi-in so far.
This also includes values we changed ourselves through sentCCToInstrument"""
instrument = _instr(idKey)
if instrument.enabled:
return instrument.midiProcessor.ccState
else:
return None
def sentCCToInstrument(idKey:tuple, ccNumber:int, value:int):
instrument = _instr(idKey)
if instrument.enabled:
instrument.scene.send_midi_event(0xB0, ccNumber, value)
instrument.midiProcessor.ccState[ccNumber] = value #midi processor doesn't get send_midi_event
else:
return None

7
engine/instrument.py

@ -384,6 +384,7 @@ class Instrument(object):
self.playableKeys = tuple(sorted(allKeys))
self.controlLabels = self.program.get_control_labels() #opcode label_cc# in <control>
self.keyLabels = self.program.get_key_labels() #opcode label_cc# in <control>
#Add some defaults.
for k,v in {60:"Middle C", 53:"𝄢", 67:"𝄞"}.items():
@ -504,6 +505,7 @@ class Instrument(object):
self.midiProcessor = MidiProcessor(parentInput = self) #works through self.cboxMidiPortUid
self.midiProcessor.register_NoteOn(self.triggerNoteOnCallback)
self.midiProcessor.register_NoteOff(self.triggerNoteOffCallback)
self.midiProcessor.register_CC(self.triggerCCCallback)
#self.midiProcessor.notePrinter(True)
self.parentLibrary.parentData.parentSession.eventLoop.fastConnect(self.midiProcessor.processEvents)
@ -560,7 +562,10 @@ class Instrument(object):
consider to change eventloop.slowConnect to fastConnect. And also disconnect in self.disable()"""
self.parentLibrary.parentData.instrumentMidiNoteOffActivity(self.idKey, pitch, velocity)
def triggerCCCallback(self, timestamp, channel, ccNumber, value):
"""args are: timestamp, channel, ccNumber, value
consider to change eventloop.slowConnect to fastConnect. And also disconnect in self.disable()"""
self.parentLibrary.parentData.instrumentCCActivity(self.idKey, ccNumber, value)
def getAvailablePorts(self)->dict:
"""This function queries JACK each time it is called.

3
engine/main.py

@ -69,7 +69,7 @@ class Data(TemplateData):
for instrId, instr in lib.instruments.items():
yield instr
def parseAndLoadInstrumentLibraries(self, baseSamplePath, instrumentMidiNoteOnActivity, instrumentMidiNoteOffActivity):
def parseAndLoadInstrumentLibraries(self, baseSamplePath, instrumentMidiNoteOnActivity, instrumentMidiNoteOffActivity, instrumentCCActivity):
"""Called first by api.startEngine, which receives the global sample path from
the GUI.
@ -170,6 +170,7 @@ class Data(TemplateData):
self.instrumentMidiNoteOnActivity = instrumentMidiNoteOnActivity # the api will inject a callback function here which takes (libId, instrId) as parameter to indicate midi noteOn activity for non-critical information like a GUI LED blinking or checking for new keyswitch states. The instruments individiual midiprocessor will call this as a parent-call.
self.instrumentMidiNoteOffActivity = instrumentMidiNoteOffActivity #see above
self.instrumentCCActivity = instrumentCCActivity #see above
if firstRun: #in case of re-scan we don't need to do this a second time. The default lib cannot be updated through the download manager and will always be present.
self._createGlobalPorts() #in its own function for readability
self._createCachedJackMetadataSorting()

BIN
engine/resources/000 - Default.tembro

Binary file not shown.

66
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(1175, 651)
MainWindow.resize(1003, 668)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.centralwidget)
@ -104,30 +104,59 @@ class Ui_MainWindow(object):
self.verticalLayout_3.setSpacing(0)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.details_scrollArea = QtWidgets.QScrollArea(self.details_groupBox)
self.details_scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)
self.details_scrollArea.setFrameShadow(QtWidgets.QFrame.Plain)
self.details_scrollArea.setLineWidth(0)
self.details_scrollArea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.details_scrollArea.setWidgetResizable(True)
self.details_scrollArea.setObjectName("details_scrollArea")
self.scrollAreaWidgetContents = QtWidgets.QWidget()
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 993, 125))
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 823, 136))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.formLayout = QtWidgets.QFormLayout(self.scrollAreaWidgetContents)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.scrollAreaWidgetContents)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.widget = QtWidgets.QWidget(self.scrollAreaWidgetContents)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth())
self.widget.setSizePolicy(sizePolicy)
self.widget.setObjectName("widget")
self.formLayout = QtWidgets.QFormLayout(self.widget)
self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
self.formLayout.setContentsMargins(-1, 20, 15, -1)
self.formLayout.setObjectName("formLayout")
self.variants_comboBox = QtWidgets.QComboBox(self.scrollAreaWidgetContents)
self.variants_comboBox.setObjectName("variants_comboBox")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.variants_comboBox)
self.variant_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.variant_label = QtWidgets.QLabel(self.widget)
self.variant_label.setObjectName("variant_label")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.variant_label)
self.info_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.variant_label)
self.variants_comboBox = QtWidgets.QComboBox(self.widget)
self.variants_comboBox.setObjectName("variants_comboBox")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.variants_comboBox)
self.keySwitch_label = QtWidgets.QLabel(self.widget)
self.keySwitch_label.setObjectName("keySwitch_label")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.keySwitch_label)
self.keySwitch_comboBox = QtWidgets.QComboBox(self.widget)
self.keySwitch_comboBox.setObjectName("keySwitch_comboBox")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.keySwitch_comboBox)
self.info_label = QtWidgets.QLabel(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.info_label.sizePolicy().hasHeightForWidth())
self.info_label.setSizePolicy(sizePolicy)
self.info_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.info_label.setWordWrap(True)
self.info_label.setObjectName("info_label")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.info_label)
self.keySwitch_comboBox = QtWidgets.QComboBox(self.scrollAreaWidgetContents)
self.keySwitch_comboBox.setObjectName("keySwitch_comboBox")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.keySwitch_comboBox)
self.keySwitch_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.keySwitch_label.setObjectName("keySwitch_label")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.keySwitch_label)
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.info_label)
self.horizontalLayout_2.addWidget(self.widget)
self.controls_groupBox = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.controls_groupBox.sizePolicy().hasHeightForWidth())
self.controls_groupBox.setSizePolicy(sizePolicy)
self.controls_groupBox.setObjectName("controls_groupBox")
self.horizontalLayout_2.addWidget(self.controls_groupBox)
self.details_scrollArea.setWidget(self.scrollAreaWidgetContents)
self.verticalLayout_3.addWidget(self.details_scrollArea)
self.verticalLayout.addWidget(self.splitter)
@ -142,7 +171,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, 1175, 20))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1003, 20))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
@ -172,5 +201,6 @@ class Ui_MainWindow(object):
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.info_label.setText(_translate("MainWindow", "TextLabel"))
self.controls_groupBox.setTitle(_translate("MainWindow", "Controls"))

82
qtgui/designer/mainwindow.ui

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1175</width>
<height>651</height>
<width>1003</width>
<height>668</height>
</rect>
</property>
<property name="windowTitle">
@ -320,6 +320,18 @@
</property>
<item>
<widget class="QScrollArea" name="details_scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
@ -328,23 +340,57 @@
<rect>
<x>0</x>
<y>0</y>
<width>993</width>
<height>125</height>
<width>823</width>
<height>136</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="1">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<property name="topMargin">
<number>20</number>
</property>
<property name="rightMargin">
<number>15</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="variant_label">
<property name="text">
<string>Variants</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="variants_comboBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="variant_label">
<widget class="QLabel" name="keySwitch_label">
<property name="text">
<string>Variants</string>
<string>KeySwitch</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="1" column="1">
<widget class="QComboBox" name="keySwitch_comboBox"/>
</item>
<item row="2" column="1">
<widget class="QLabel" name="info_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>TextLabel</string>
</property>
@ -356,13 +402,19 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="keySwitch_comboBox"/>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="keySwitch_label">
<property name="text">
<string>KeySwitch</string>
<item>
<widget class="QGroupBox" name="controls_groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Controls</string>
</property>
</widget>
</item>
@ -401,7 +453,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1175</width>
<width>1003</width>
<height>20</height>
</rect>
</property>

102
qtgui/selectedinstrumentcontroller.py

@ -28,6 +28,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets
#Our Qt
from .instrument import GuiInstrument, GuiLibrary #for the types
from template.qtgui.flowlayout import FlowLayout
#Engine
import engine.api as api
@ -48,18 +50,23 @@ class SelectedInstrumentController(object):
self.currentIdKey = None
self.engineData = {} # idKey tuple : engine library metadata dict.
self.statusUpdates = {} # same as engineData, but with incremental status updates. One is guranteed to exist at startup
self.controlWidgets = {} # ccNumber or other identifier : ControlWidget (our class from this file)
#Our Widgets
self.ui = parentMainWindow.ui
self.ui.details_groupBox.setTitle("")
self.ui.details_scrollArea.hide() #until the first instrument was selected
self.ui.controls_groupBox.setLayout(FlowLayout(margin=1))
self.ui.variants_comboBox.activated.connect(self._newVariantChosen)
self.ui.keySwitch_comboBox.activated.connect(self._newKeySwitchChosen)
#Callbacks
api.callbacks.instrumentListMetadata.append(self.react_initialInstrumentList)
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
api.callbacks.instrumentCCActivity.append(self.react_instrumentCCActivity)
def directLibrary(self, idKey:tuple):
"""User clicked on a library treeItem"""
@ -69,6 +76,8 @@ class SelectedInstrumentController(object):
self.ui.variants_comboBox.hide()
self.ui.variant_label.hide()
self.ui.controls_groupBox.hide()
self.ui.keySwitch_label.hide()
self.ui.keySwitch_comboBox.hide()
@ -129,6 +138,33 @@ class SelectedInstrumentController(object):
self.ui.keySwitch_comboBox.setCurrentIndex(curIdx)
def _populateControls(self, instrumentStatus:dict):
"""Remove all CC Knobs, Faders and other control widgets and draw them again for
the current GUI-Instrument.
This is called when activating an instrument, switching the variant or simply
coming back to an already loaded instrument in the GUI."""
for controlWidget in self.controlWidgets.values():
self.ui.controls_groupBox.layout().removeWidget(controlWidget)
controlWidget.hide()
controlWidget.setParent(None)
del controlWidget
self.controlWidgets = {}
if not instrumentStatus["state"] or not instrumentStatus["controlLabels"] : #Not activated yet.
return
ccNow = api.ccTrackingState(instrumentStatus["idKey"])
for ccNumber, ccLabel in instrumentStatus["controlLabels"].items():
w = ControlWidget(self.ui.controls_groupBox, instrumentStatus, ccNumber, ccLabel)
self.controlWidgets[ccNumber] = w
self.ui.controls_groupBox.layout().addWidget(w)
if ccNumber in ccNow:
w.setValue(ccNow[ccNumber], sendToEngine=False) #don't send the engine values it already knows.
def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem):
"""
Program wide GUI-only callback from
@ -179,6 +215,9 @@ class SelectedInstrumentController(object):
self.ui.variants_comboBox.clear()
self.ui.variants_comboBox.addItems(instrumentData["variantsWithoutSfzExtension"])
self.ui.controls_groupBox.show()
self._populateControls(instrumentStatus)
self.ui.info_label.setText(self._metadataToDescriptionLabel(instrumentData))
#Dynamic
@ -232,6 +271,7 @@ class SelectedInstrumentController(object):
self.ui.variants_comboBox.setCurrentIndex(defaultVariantIndex)
self._populateKeySwitchComboBox(instrumentStatus)
self._populateControls(instrumentStatus)
def react_initialInstrumentList(self, data:dict):
"""For data form see docstring of instrument.py buildTree()
@ -246,3 +286,65 @@ class SelectedInstrumentController(object):
self.react_instrumentStatusChanged
"""
self.engineData = data
def react_instrumentCCActivity(self, idKey, ccNumber, value):
if not idKey == self.currentIdKey:
return #not for us
if ccNumber in self.controlWidgets:
self.controlWidgets[ccNumber].setValue(value)
class ControlWidget(QtWidgets.QWidget):
def __init__(self, parentWidget, instrumentStatus, ccNumber, ccLabel):
super().__init__(parentWidget)
self.instrumentStatus = instrumentStatus #static info about the complete instrument.
assert self.instrumentStatus["state"], instrumentStatus
assert self.instrumentStatus["controlLabels"], instrumentStatus
self.idKey = instrumentStatus["idKey"]
self.ccNumber = ccNumber
self.setLayout(QtWidgets.QVBoxLayout())
self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.slider.setTracking(True)
self.slider.valueChanged.connect(lambda v: self.setValue(v, sendToEngine=True))
self.layout().addWidget(self.slider)
self.spinBox = QtWidgets.QSpinBox()
self.spinBox.valueChanged.connect(lambda v: self.setValue(v, sendToEngine=True))
self.spinBox.setPrefix(f"[{ccNumber}] ")
self.layout().addWidget(self.spinBox)
self.label = QtWidgets.QLabel(ccLabel)
self.layout().addWidget(self.label)
self.setFixedSize(132,96)
self.setRange(0,127)
#self.setValue(0) #No need. We don't want to destroy the instruments defaults.
self.spinBox.setSpecialValueText(f"[CC {ccNumber}]") #Never touched by human hands
def setRange(self, floor:int, ceiling:int):
self.spinBox.setRange(floor, ceiling)
self.slider.setRange(floor, ceiling)
def setValue(self, value:int, sendToEngine=False):
self.spinBox.blockSignals(True)
self.slider.blockSignals(True)
self.spinBox.setSpecialValueText("") #0 is 0
self.spinBox.setValue(value)
self.slider.setValue(value)
#Send to Engine
if sendToEngine:
api.sentCCToInstrument(self.idKey, self.ccNumber, value)
self.spinBox.blockSignals(False)
self.slider.blockSignals(False)

159
template/qtgui/flowlayout.py

@ -0,0 +1,159 @@
#!/usr/bin/env python
#############################################################################
##
## Copyright (C) 2013 Riverbank Computing Limited.
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
## All rights reserved.
##
## This file is part of the examples of PyQt.
##
## $QT_BEGIN_LICENSE:BSD$
## You may use this file under the terms of the BSD license as follows:
##
## "Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are
## met:
## * Redistributions of source code must retain the above copyright
## notice, this list of conditions and the following disclaimer.
## * Redistributions in binary form must reproduce the above copyright
## notice, this list of conditions and the following disclaimer in
## the documentation and/or other materials provided with the
## distribution.
## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
## the names of its contributors may be used to endorse or promote
## products derived from this software without specific prior written
## permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
## $QT_END_LICENSE$
##
#############################################################################
from PyQt5.QtCore import QPoint, QRect, QSize, Qt
from PyQt5.QtWidgets import (QApplication, QLayout, QPushButton, QSizePolicy,
QWidget)
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
flowLayout = FlowLayout()
flowLayout.addWidget(QPushButton("Short"))
flowLayout.addWidget(QPushButton("Longer"))
flowLayout.addWidget(QPushButton("Different text"))
flowLayout.addWidget(QPushButton("More text"))
flowLayout.addWidget(QPushButton("Even longer button text"))
self.setLayout(flowLayout)
self.setWindowTitle("Flow Layout")
class FlowLayout(QLayout):
def __init__(self, parent=None, margin=0, spacing=-1):
super(FlowLayout, self).__init__(parent)
if parent is not None:
self.setContentsMargins(margin, margin, margin, margin)
self.setSpacing(spacing)
self.itemList = []
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
def addItem(self, item):
self.itemList.append(item)
def count(self):
return len(self.itemList)
def itemAt(self, index):
if index >= 0 and index < len(self.itemList):
return self.itemList[index]
return None
def takeAt(self, index):
if index >= 0 and index < len(self.itemList):
return self.itemList.pop(index)
return None
def expandingDirections(self):
return Qt.Orientations(Qt.Orientation(0))
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
height = self.doLayout(QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self.doLayout(rect, False)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self.itemList:
size = size.expandedTo(item.minimumSize())
margin, _, _, _ = self.getContentsMargins()
size += QSize(2 * margin, 2 * margin)
return size
def doLayout(self, rect, testOnly):
x = rect.x()
y = rect.y()
lineHeight = 0
for item in self.itemList:
wid = item.widget()
spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
nextX = x + item.sizeHint().width() + spaceX
if nextX - spaceX > rect.right() and lineHeight > 0:
x = rect.x()
y = y + lineHeight + spaceY
nextX = x + item.sizeHint().width() + spaceX
lineHeight = 0
if not testOnly:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = nextX
lineHeight = max(lineHeight, item.sizeHint().height())
return y + lineHeight - rect.y()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWin = Window()
mainWin.show()
sys.exit(app.exec_())
Loading…
Cancel
Save