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`.
This commit is contained in:
Michael Gregorius
2023-07-08 12:11:46 +02:00
parent 1295deb3a8
commit 610fb3442f
6 changed files with 327 additions and 66 deletions

57
include/BarModelEditor.h Normal file
View File

@@ -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 <QWidget>
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;
};
}

View File

@@ -29,6 +29,8 @@
#include <QLabel>
#include <QScrollArea>
#include <QScrollBar>
#include <QSpacerItem>
#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<QWidget *>())
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("<b>" + tr("Parameter") + "</b>", 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("<b>" + tr("Link") + "</b>", 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("<b>" + channelString.arg(QString::number(i + 1)) + "</b>", widget), 0, currentChannelColumn, Qt::AlignHCenter);
// First add the channel header with the channel number
gridLayout->addWidget(new QLabel("<b>" + tr("Channel %1").arg(QString::number(i + 1)) + "</b>", 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("<b>" + tr("Link") + "</b>", 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<LadspaControls *>(m_effectControls);
}
ch_cnt_t LadspaMatrixControlDialog::getChannelCount() const
{
return getLadspaControls()->m_processors;
}
} // namespace lmms::gui

View File

@@ -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;
};

View File

@@ -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

View File

@@ -29,10 +29,12 @@
#include "LadspaControl.h"
#include "LadspaBase.h"
#include "BarModelEditor.h"
#include "LedCheckBox.h"
#include "TempoSyncKnob.h"
#include <QLayout>
#include <QLabel>
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;
}

View File

@@ -0,0 +1,115 @@
#include <BarModelEditor.h>
#include "CaptionMenu.h"
#include <QPainter>
#include <QInputDialog>
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()));
}
}
}