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:
57
include/BarModelEditor.h
Normal file
57
include/BarModelEditor.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
115
src/gui/widgets/BarModelEditor.cpp
Normal file
115
src/gui/widgets/BarModelEditor.cpp
Normal 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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user