From 610fb3442fe316fe611001963fcbc8a44f09857b Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 8 Jul 2023 12:11:46 +0200 Subject: [PATCH] Add BarModelEditor and improve layouts Add the new class `BarModelEditor` which is intended to become a new way to adjust values of float models. Simplify the layout in `LadspaMatrixControlDialog` by removing some nested layouts. Remove the "Parameters" column. Adjust `LadspaMatrixControlView` to implement the following changes: * Show the name of the control next to toggle buttons (`LedCheckBox`). * Use the new `BarModelEditor` for integer and float types. * SHow the name of the control next to time based parameters that use `TempoSyncKnob`. The names are shown so that the "Parameters" column can be removed. Technical details ------------------ The class `LadspaMatrixControlDialog` now creates a widget that contains the matrix layout with the controls. This widget is then added to a scroll area. The layout is populated in the new method `arrangeControls`. Add some helper methods to `LadspaMatrixControlDialog` which retrieve the `LadspaControls` instance and the number of channels. Add the implementation of `BarModelEditor` to `src/gui/CMakeLists.txt`. TODOs ------ Extract common code out of the `Knob` class so that it can be reused by `BarModelEditor`. --- include/BarModelEditor.h | 57 ++++++ .../LadspaMatrixControlDialog.cpp | 179 ++++++++++++------ .../LadspaEffect/LadspaMatrixControlDialog.h | 28 ++- src/gui/CMakeLists.txt | 1 + src/gui/LadspaMatrixControlView.cpp | 13 +- src/gui/widgets/BarModelEditor.cpp | 115 +++++++++++ 6 files changed, 327 insertions(+), 66 deletions(-) create mode 100644 include/BarModelEditor.h create mode 100644 src/gui/widgets/BarModelEditor.cpp diff --git a/include/BarModelEditor.h b/include/BarModelEditor.h new file mode 100644 index 000000000..0ffb27199 --- /dev/null +++ b/include/BarModelEditor.h @@ -0,0 +1,57 @@ +/* + * BarModelEditor.h - edit model values using a bar display + * + * Copyright (c) 2023-now Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program 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 2 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "AutomatableModelView.h" + +#include + +namespace lmms::gui +{ + +class BarModelEditor : public QWidget, public FloatModelView +{ +public: + BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + + // Define how the widget will behave in a layout + QSizePolicy sizePolicy() const; + + virtual QSize minimumSizeHint() const override; + + virtual QSize sizeHint() const override; + +protected: + virtual void paintEvent(QPaintEvent *event) override; + + virtual void contextMenuEvent(QContextMenuEvent * me) override; + virtual void mouseDoubleClickEvent(QMouseEvent * me) override; + +private: + void connectToModelSignals(); + +private: + QString const m_text; +}; + +} diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp index d34463e3b..3d2a14556 100644 --- a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp @@ -29,6 +29,8 @@ #include #include #include +#include + #include "LadspaBase.h" #include "LadspaControl.h" @@ -43,110 +45,153 @@ namespace lmms::gui { -static int const s_linkBaseColumn = 0; -static int const s_parameterNameBaseColumn = 2; -static int const s_channelBaseColumn = 4; - - - - LadspaMatrixControlDialog::LadspaMatrixControlDialog(LadspaControls * ladspaControls) : EffectControlDialog(ladspaControls), - m_effectGridLayout(nullptr), + m_scrollArea(nullptr), m_stereoLink(nullptr) { QVBoxLayout * mainLayout = new QVBoxLayout(this); - m_effectGridLayout = new QGridLayout(); - mainLayout->addLayout(m_effectGridLayout); + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(true); + m_scrollArea->setFrameShape(QFrame::NoFrame); + // Add a scroll area that grows + mainLayout->addWidget(m_scrollArea, 1); + + // Populate the parameter matrix and put it into the scroll area updateEffectView(ladspaControls); - if (ladspaControls->m_processors > 1) + // Add button to link all channels if there's more than one channel + if (getChannelCount() > 1) { mainLayout->addSpacing(3); - QHBoxLayout * center = new QHBoxLayout(); - mainLayout->addLayout(center); + m_stereoLink = new LedCheckBox(tr("Link Channels"), this); m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); - center->addWidget(m_stereoLink); + mainLayout->addWidget(m_stereoLink, 0, Qt::AlignCenter); } } - - - -void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls) +bool LadspaMatrixControlDialog::needsLinkColumn() const { - for (auto child : findChildren()) + LadspaControls * ladspaControls = getLadspaControls(); + + ch_cnt_t const channelCount = getChannelCount(); + for (ch_cnt_t i = 0; i < channelCount; ++i) { - delete child; + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + if (ladspaControl->m_link) + { + return true; + } + } } - m_effectControls = ladspaControls; + return false; +} - QWidget *widget = new QWidget(this); - QGridLayout *gridLayout = new QGridLayout(widget); - widget->setLayout(gridLayout); +void LadspaMatrixControlDialog::arrangeControls(QWidget * parent, QGridLayout* gridLayout) +{ + LadspaControls * ladspaControls = getLadspaControls(); - gridLayout->addWidget(new QLabel("" + tr("Parameter") + "", widget), 0, s_parameterNameBaseColumn, Qt::AlignRight); + int const headerRow = 0; + int const linkColumn = 0; - bool linkLabelAdded = false; - ch_cnt_t const numberOfChannels = ladspaControls->m_processors; + bool const linkColumnNeeded = needsLinkColumn(); + if (linkColumnNeeded) + { + gridLayout->addWidget(new QLabel("" + tr("Link") + "", parent), headerRow, linkColumn, Qt::AlignHCenter); - gridLayout->setColumnMinimumWidth(1, 20); + // If there's a link column then it should not stretch + gridLayout->setColumnStretch(linkColumn, 0); + } + int const channelStartColumn = linkColumnNeeded ? 1 : 0; + + // The header row should not grow vertically + gridLayout->setRowStretch(0, 0); + + // Records the maximum row with parameters so that we can add a vertical spacer after that row + int maxRow = 0; + + // Iterate the channels and add widgets for each control + ch_cnt_t const numberOfChannels = getChannelCount(); for (ch_cnt_t i = 0; i < numberOfChannels; ++i) { - QString channelString(tr("Channel %1")); - int currentChannelColumn = s_channelBaseColumn + 2*i; + int currentChannelColumn = channelStartColumn + i; + gridLayout->setColumnStretch(currentChannelColumn, 1); - gridLayout->addWidget(new QLabel("" + channelString.arg(QString::number(i + 1)) + "", widget), 0, currentChannelColumn, Qt::AlignHCenter); + // First add the channel header with the channel number + gridLayout->addWidget(new QLabel("" + tr("Channel %1").arg(QString::number(i + 1)) + "", parent), headerRow, currentChannelColumn, Qt::AlignHCenter); int currentRow = 1; - for (auto ladspaControl : ladspaControls->m_controls[i]) + + if (i == 0) { - if (i == 0) + // Configure the current parameter row to not stretch. + // Only do this once, i.e. when working with the first channel. + gridLayout->setRowStretch(currentRow, 0); + } + + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + // Only use the first channel to determine if we need to add link controls + if (i == 0 && ladspaControl->m_link) { // TODO Assumes that all processors are equal! Change to more general approach, e.g. map from name to row - - // Link - if (ladspaControl->m_link) - { - if (!linkLabelAdded) - { - gridLayout->addWidget(new QLabel("" + tr("Link") + "", widget), 0, s_linkBaseColumn, Qt::AlignHCenter); - linkLabelAdded = true; - } - LedCheckBox * linkCheckBox = new LedCheckBox("", widget); - linkCheckBox->setModel(&ladspaControl->m_linkEnabledModel); - linkCheckBox->setToolTip(tr("Link channels")); - gridLayout->addWidget(linkCheckBox, currentRow, s_linkBaseColumn, Qt::AlignHCenter); - } - - // Parameter name - QString portName = ladspaControl->port()->name; - QLabel *portNameLabel = new QLabel(portName, widget); - gridLayout->addWidget(portNameLabel, currentRow, s_parameterNameBaseColumn, Qt::AlignRight); + LedCheckBox * linkCheckBox = new LedCheckBox("", parent); + linkCheckBox->setModel(&ladspaControl->m_linkEnabledModel); + linkCheckBox->setToolTip(tr("Link channels")); + gridLayout->addWidget(linkCheckBox, currentRow, linkColumn, Qt::AlignHCenter); } - LadspaMatrixControlView *ladspaMatrixControlView = new LadspaMatrixControlView(widget, ladspaControl); - gridLayout->addWidget(ladspaMatrixControlView, currentRow, currentChannelColumn, Qt::AlignHCenter); - gridLayout->setColumnMinimumWidth(currentChannelColumn - 1, 20); + // TODO Use a factory to directly create the widgets? Currently they are wrapped in another layout in LadspaMatrixControlView... + LadspaMatrixControlView *ladspaMatrixControlView = new LadspaMatrixControlView(parent, ladspaControl); + gridLayout->addWidget(ladspaMatrixControlView, currentRow, currentChannelColumn); + + // Record the maximum row so that we add a vertical spacer after that row + maxRow = std::max(maxRow, currentRow); ++currentRow; } } - QScrollArea *scrollArea = new QScrollArea(this); - scrollArea->setWidgetResizable(true); - scrollArea->setWidget(widget); - scrollArea->setFrameShape(QFrame::NoFrame); + // Add a spacer item after the maximum row + QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + gridLayout->addItem(spacer, maxRow + 1, 0); +} - m_effectGridLayout->addWidget(scrollArea, 0, 0); - m_effectGridLayout->setMargin(0); +QWidget * LadspaMatrixControlDialog::createMatrixWidget() +{ + QWidget *widget = new QWidget(this); + QGridLayout *gridLayout = new QGridLayout(widget); + widget->setLayout(gridLayout); - if (numberOfChannels > 1 && m_stereoLink != nullptr) + arrangeControls(widget, gridLayout); + + return widget; +} + +void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls) +{ + m_effectControls = ladspaControls; + + // No need to delete the existing widget as it's deleted + // by the scroll view when we replace it. + QWidget * matrixWidget = createMatrixWidget(); + m_scrollArea->setWidget(matrixWidget); + + // Make sure that the horizontal scroll bar does not show + // From: https://forum.qt.io/topic/13374/solved-qscrollarea-vertical-scroll-only/4 + m_scrollArea->setMinimumWidth(matrixWidget->minimumSizeHint().width() + m_scrollArea->verticalScrollBar()->width()); + + if (getChannelCount() > 1 && m_stereoLink != nullptr) { m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); } @@ -156,4 +201,14 @@ void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls Qt::DirectConnection); } +LadspaControls * LadspaMatrixControlDialog::getLadspaControls() const +{ + return dynamic_cast(m_effectControls); +} + +ch_cnt_t LadspaMatrixControlDialog::getChannelCount() const +{ + return getLadspaControls()->m_processors; +} + } // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.h b/plugins/LadspaEffect/LadspaMatrixControlDialog.h index a1564d1e6..8ef9428b8 100644 --- a/plugins/LadspaEffect/LadspaMatrixControlDialog.h +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.h @@ -28,8 +28,11 @@ #include "EffectControlDialog.h" +#include "lmms_basics.h" + class QGridLayout; +class QScrollArea; namespace lmms { @@ -53,9 +56,32 @@ public: private slots: void updateEffectView(LadspaControls* ctl); +private: + /** + * @brief Checks if a link column is needed for the current effect controls. + * @return true if a link column is needed. + */ + bool needsLinkColumn() const; + + /** + * @brief Arranges widgets for the current controls in a grid/matrix layout. + * @param parent The parent of all created widgets + * @param gridLayout The layout into which the controls are organized + */ + void arrangeControls(QWidget * parent, QGridLayout* gridLayout); + + /** + * @brief Creates a widget that holds the widgets of the current controls in a matrix arrangement. + * @param ladspaControls + * @return + */ + QWidget * createMatrixWidget(); + + LadspaControls * getLadspaControls() const; + ch_cnt_t getChannelCount() const; private: - QGridLayout* m_effectGridLayout; + QScrollArea* m_scrollArea; LedCheckBox* m_stereoLink; }; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index c73ea126b..3db69aec8 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -95,6 +95,7 @@ SET(LMMS_SRCS gui/widgets/AutomatableButton.cpp gui/widgets/AutomatableSlider.cpp + gui/widgets/BarModelEditor.cpp gui/widgets/CPULoadWidget.cpp gui/widgets/CaptionMenu.cpp gui/widgets/ComboBox.cpp diff --git a/src/gui/LadspaMatrixControlView.cpp b/src/gui/LadspaMatrixControlView.cpp index fc9aa9d75..96ae96db9 100644 --- a/src/gui/LadspaMatrixControlView.cpp +++ b/src/gui/LadspaMatrixControlView.cpp @@ -29,10 +29,12 @@ #include "LadspaControl.h" #include "LadspaBase.h" +#include "BarModelEditor.h" #include "LedCheckBox.h" #include "TempoSyncKnob.h" #include +#include namespace lmms::gui @@ -51,12 +53,13 @@ LadspaMatrixControlView::LadspaMatrixControlView(QWidget * parent, Knob * knob = nullptr; buffer_data_t dataType = m_ladspaControl->port()->data_type; + QString const name = m_ladspaControl->port()->name; switch (dataType) { case TOGGLED: { LedCheckBox * toggle = new LedCheckBox( - "", this, QString(), LedCheckBox::Green); + name, this, QString(), LedCheckBox::Green); toggle->setModel(m_ladspaControl->toggledModel()); layout->addWidget(toggle); setFixedSize(toggle->width(), toggle->height()); @@ -65,16 +68,20 @@ LadspaMatrixControlView::LadspaMatrixControlView(QWidget * parent, case INTEGER: case FLOATING: - knob = new Knob(knobBright_26, this, m_ladspaControl->port()->name); + /*knob = new Knob(knobBright_26, this, name); knob->setModel(m_ladspaControl->knobModel()); + knob->setLabel(name);*/ + layout->addWidget(new BarModelEditor(name, m_ladspaControl->knobModel(), this)); break; case TIME: - knob = new TempoSyncKnob(knobBright_26, this, m_ladspaControl->port()->name); + knob = new TempoSyncKnob(knobBright_26, this, name); knob->setModel(m_ladspaControl->tempoSyncKnobModel()); + knob->setLabel(name); break; default: + layout->addWidget(new QLabel(tr("%1 (unsupported)").arg(name), this)); break; } diff --git a/src/gui/widgets/BarModelEditor.cpp b/src/gui/widgets/BarModelEditor.cpp new file mode 100644 index 000000000..f8b8fae5b --- /dev/null +++ b/src/gui/widgets/BarModelEditor.cpp @@ -0,0 +1,115 @@ +#include + +#include "CaptionMenu.h" + +#include +#include + + +namespace lmms::gui +{ + +BarModelEditor::BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + QWidget(parent), + FloatModelView( floatModel, this ), + m_text(text) +{ + connectToModelSignals(); +} + +QSizePolicy BarModelEditor::sizePolicy() const +{ + return QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize BarModelEditor::minimumSizeHint() const +{ + auto const fm = fontMetrics(); + return QSize(200, fm.height() * 1.3); +} + +QSize BarModelEditor::sizeHint() const +{ + return minimumSizeHint(); +} + +void BarModelEditor::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + QColor const background(30, 40, 51); + QColor const foreground(3, 94, 97); + QColor const textColor(14, 192, 198); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + QRect const r = rect(); + + QPainter painter(this); + painter.setPen(background); + painter.setBrush(background); + painter.drawRect(r); + + // Compute the percentage + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + int const margin = 2; + QMargins const margins(margin, margin, margin, margin); + QRect const valueRect = r.marginsRemoved(margins); + + painter.setPen(foreground); + painter.setBrush(foreground); + painter.drawRect(QRect(valueRect.topLeft(), QPoint(valueRect.width() * percentage, valueRect.height()))); + + // Draw text + QRect const textRect = valueRect.marginsRemoved(margins); + painter.setPen(textColor); + painter.drawText(textRect, m_text); +} + +void BarModelEditor::contextMenuEvent(QContextMenuEvent * me) +{ + CaptionMenu contextMenu(model()->displayName(), this); + + addDefaultActions(&contextMenu); + + contextMenu.addSeparator(); + contextMenu.exec(QCursor::pos()); +} + +void BarModelEditor::mouseDoubleClickEvent(QMouseEvent * me) +{ + bool ok; + + float new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "%1 and %2:"). + arg(model()->minValue()). + arg(model()->maxValue()), + model()->getRoundedValue(), + model()->minValue(), + model()->maxValue(), model()->getDigitCount(), &ok); + + if (ok) + { + model()->setValue(new_val); + } +} + +void BarModelEditor::connectToModelSignals() +{ + auto * m = model(); + if(m) + { + // TODO The first connection does a "friendly" update in Knob. Do we also have to do this? + QObject::connect(m, SIGNAL(dataChanged()), this, SLOT(update())); + QObject::connect(m ,SIGNAL(propertiesChanged()), this, SLOT(update())); + } +} + +}