Linked model groups (#4964)

Add labeled controls for different types with a common base class

Implement a container for multiple equal groups of linked models and
suiting views. Such groups are suited for representing mono effects where each
Model occurs twice. A group provides Models for one mono processor and is
visually represented with a group box.

This concept is common for LADSPA and Lv2, and useful for any mono effect.
This commit is contained in:
Johannes Lorenz
2020-02-21 19:26:29 +01:00
committed by GitHub
parent 3410db4d99
commit eebdc0f4be
11 changed files with 1351 additions and 0 deletions

View File

@@ -31,6 +31,7 @@ set(LMMS_SRCS
core/LadspaControl.cpp
core/LadspaManager.cpp
core/LfoController.cpp
core/LinkedModelGroups.cpp
core/LocklessAllocator.cpp
core/MemoryHelper.cpp
core/MemoryManager.cpp

View File

@@ -0,0 +1,185 @@
/*
* LinkedModelGroups.cpp - base classes for groups of linked models
*
* Copyright (c) 2019-2019 Johannes Lorenz <j.git$$$lorenz-ho.me, $$$=@>
*
* 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 "LinkedModelGroups.h"
#include <QDomDocument>
#include <QDomElement>
#include "AutomatableModel.h"
#include "stdshims.h"
/*
LinkedModelGroup
*/
void LinkedModelGroup::linkControls(LinkedModelGroup *other)
{
foreach_model([&other](const std::string& id, ModelInfo& inf)
{
auto itr2 = other->m_models.find(id);
Q_ASSERT(itr2 != other->m_models.end());
AutomatableModel::linkModels(inf.m_model, itr2->second.m_model);
});
}
void LinkedModelGroup::saveValues(QDomDocument &doc, QDomElement &that)
{
foreach_model([&doc, &that](const std::string& , ModelInfo& inf)
{
inf.m_model->saveSettings(doc, that, /*m_models[idx].m_name*/ inf.m_name); /* TODO: m_name useful */
});
}
void LinkedModelGroup::loadValues(const QDomElement &that)
{
foreach_model([&that](const std::string& , ModelInfo& inf)
{
// try to load, if it fails, this will load a sane initial value
inf.m_model->loadSettings(that, /*m_models()[idx].m_name*/ inf.m_name); /* TODO: m_name useful */
});
}
void LinkedModelGroup::addModel(AutomatableModel *model, const QString &name)
{
model->setObjectName(name);
m_models.emplace(std::string(name.toUtf8().data()), ModelInfo(name, model));
connect(model, &AutomatableModel::destroyed,
this, [this, model](jo_id_t){
if(containsModel(model->objectName()))
{
emit modelRemoved(model);
eraseModel(model->objectName());
}
},
Qt::DirectConnection);
// View needs to create another child view, e.g. a new knob:
emit modelAdded(model);
emit dataChanged();
}
void LinkedModelGroup::removeControl(AutomatableModel* mdl)
{
if(containsModel(mdl->objectName()))
{
emit modelRemoved(mdl);
eraseModel(mdl->objectName());
}
}
bool LinkedModelGroup::eraseModel(const QString& name)
{
return m_models.erase(name.toStdString()) > 0;
}
void LinkedModelGroup::clearModels()
{
m_models.clear();
}
bool LinkedModelGroup::containsModel(const QString &name) const
{
return m_models.find(name.toStdString()) != m_models.end();
}
/*
LinkedModelGroups
*/
LinkedModelGroups::~LinkedModelGroups() {}
void LinkedModelGroups::linkAllModels()
{
LinkedModelGroup* first = getGroup(0);
LinkedModelGroup* cur;
for (std::size_t i = 1; (cur = getGroup(i)); ++i)
{
first->linkControls(cur);
}
}
void LinkedModelGroups::saveSettings(QDomDocument& doc, QDomElement& that)
{
LinkedModelGroup* grp0 = getGroup(0);
if (grp0)
{
QDomElement models = doc.createElement("models");
that.appendChild(models);
grp0->saveValues(doc, models);
}
else { /* don't even add a "models" node */ }
}
void LinkedModelGroups::loadSettings(const QDomElement& that)
{
QDomElement models = that.firstChildElement("models");
LinkedModelGroup* grp0;
if (!models.isNull() && (grp0 = getGroup(0)))
{
// only load the first group, the others are linked to the first
grp0->loadValues(models);
}
}

View File

@@ -52,6 +52,7 @@ SET(LMMS_SRCS
gui/widgets/ComboBox.cpp
gui/widgets/ControllerRackView.cpp
gui/widgets/ControllerView.cpp
gui/widgets/Controls.cpp
gui/widgets/CPULoadWidget.cpp
gui/widgets/EffectRackView.cpp
gui/widgets/EffectView.cpp
@@ -71,6 +72,8 @@ SET(LMMS_SRCS
gui/widgets/LcdSpinBox.cpp
gui/widgets/LcdWidget.cpp
gui/widgets/LedCheckbox.cpp
gui/widgets/ControlLayout.cpp
gui/widgets/LinkedModelGroupViews.cpp
gui/widgets/MeterDialog.cpp
gui/widgets/MidiPortMenu.cpp
gui/widgets/NStateButton.cpp

View File

@@ -0,0 +1,308 @@
/*
* ControlLayout.cpp - implementation for ControlLayout.h
*
* Copyright (c) 2019-2019 Johannes Lorenz <j.git$$$lorenz-ho.me, $$$=@>
*
* 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.
*
*/
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, 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 The Qt Company Ltd 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$
**
****************************************************************************/
#include "ControlLayout.h"
#include <QWidget>
#include <QLayoutItem>
#include <QLineEdit>
#include <QRect>
#include <QString>
constexpr const int ControlLayout::m_minWidth;
ControlLayout::ControlLayout(QWidget *parent, int margin, int hSpacing, int vSpacing)
: QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing),
m_searchBar(new QLineEdit(parent))
{
setContentsMargins(margin, margin, margin, margin);
m_searchBar->setPlaceholderText("filter");
m_searchBar->setObjectName(s_searchBarName);
connect(m_searchBar, SIGNAL(textChanged(const QString&)),
this, SLOT(onTextChanged(const QString& )));
addWidget(m_searchBar);
m_searchBar->setHidden(true); // nothing to filter yet
}
ControlLayout::~ControlLayout()
{
QLayoutItem *item;
while ((item = takeAt(0))) { delete item; }
}
void ControlLayout::onTextChanged(const QString&)
{
invalidate();
update();
}
void ControlLayout::addItem(QLayoutItem *item)
{
QWidget* widget = item->widget();
const QString str = widget ? widget->objectName() : QString("unnamed");
m_itemMap.insert(str, item);
invalidate();
}
int ControlLayout::horizontalSpacing() const
{
if (m_hSpace >= 0) { return m_hSpace; }
else
{
return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
}
}
int ControlLayout::verticalSpacing() const
{
if (m_vSpace >= 0) { return m_vSpace; }
else
{
return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
}
}
int ControlLayout::count() const
{
return m_itemMap.size() - 1;
}
QMap<QString, QLayoutItem*>::const_iterator
ControlLayout::pairAt(int index) const
{
if (index < 0) { return m_itemMap.cend(); }
auto skip = [&](QLayoutItem* item) -> bool
{
return item->widget()->objectName() == s_searchBarName;
};
QMap<QString, QLayoutItem*>::const_iterator itr = m_itemMap.cbegin();
for (; itr != m_itemMap.cend() && (index > 0 || skip(itr.value())); ++itr)
{
if(!skip(itr.value())) { index--; }
}
return itr;
}
// linear time :-(
QLayoutItem *ControlLayout::itemAt(int index) const
{
auto itr = pairAt(index);
return (itr == m_itemMap.end()) ? nullptr : itr.value();
}
QLayoutItem *ControlLayout::itemByString(const QString &key) const
{
auto itr = m_itemMap.find(key);
return (itr == m_itemMap.end()) ? nullptr : *itr;
}
// linear time :-(
QLayoutItem *ControlLayout::takeAt(int index)
{
auto itr = pairAt(index);
return (itr == m_itemMap.end()) ? nullptr : m_itemMap.take(itr.key());
}
Qt::Orientations ControlLayout::expandingDirections() const
{
return Qt::Orientations();
}
bool ControlLayout::hasHeightForWidth() const
{
return true;
}
int ControlLayout::heightForWidth(int width) const
{
int height = doLayout(QRect(0, 0, width, 0), true);
return height;
}
void ControlLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
doLayout(rect, false);
}
QSize ControlLayout::sizeHint() const
{
return minimumSize();
}
QSize ControlLayout::minimumSize() const
{
// original formula from Qt's FlowLayout example:
// get maximum height and width for all children.
// as Qt will later call heightForWidth, only the width here really matters
QSize size;
for (const QLayoutItem *item : qAsConst(m_itemMap))
{
size = size.expandedTo(item->minimumSize());
}
const QMargins margins = contentsMargins();
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
// the original formula would leed to ~1 widget per row
// bash it at least to 400 so we have ~4 knobs per row
size.setWidth(qMax(size.width(), m_minWidth));
return size;
}
int ControlLayout::doLayout(const QRect &rect, bool testOnly) const
{
int left, top, right, bottom;
getContentsMargins(&left, &top, &right, &bottom);
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
int x = effectiveRect.x();
int y = effectiveRect.y();
int lineHeight = 0;
const QString filterText = m_searchBar->text();
bool first = true;
QMapIterator<QString, QLayoutItem*> itr(m_itemMap);
while (itr.hasNext())
{
itr.next();
QLayoutItem* item = itr.value();
QWidget *wid = item->widget();
if (wid)
{
if ( first || // do not filter search bar
filterText.isEmpty() || // no filter - pass all
itr.key().contains(filterText, Qt::CaseInsensitive))
{
if (first)
{
// for the search bar, only show it if there are at least
// two control widgets (i.e. at least 3 widgets)
if (m_itemMap.size() > 2) { wid->show(); }
else { wid->hide(); }
}
else { wid->show(); }
int spaceX = horizontalSpacing();
if (spaceX == -1)
{
spaceX = wid->style()->layoutSpacing(
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
}
int spaceY = verticalSpacing();
if (spaceY == -1)
{
spaceY = wid->style()->layoutSpacing(
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
}
int nextX = x + item->sizeHint().width() + spaceX;
if (nextX - spaceX > effectiveRect.right() && lineHeight > 0)
{
x = effectiveRect.x();
y = y + lineHeight + spaceY;
nextX = x + item->sizeHint().width() + spaceX;
lineHeight = 0;
}
if (!testOnly)
{
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
}
x = nextX;
lineHeight = qMax(lineHeight, item->sizeHint().height());
first = false;
}
else
{
wid->hide();
}
}
}
return y + lineHeight - rect.y() + bottom;
}
int ControlLayout::smartSpacing(QStyle::PixelMetric pm) const
{
QObject *parent = this->parent();
if (!parent) { return -1; }
else if (parent->isWidgetType())
{
QWidget *pw = static_cast<QWidget *>(parent);
return pw->style()->pixelMetric(pm, nullptr, pw);
}
else { return static_cast<QLayout *>(parent)->spacing(); }
}

View File

@@ -0,0 +1,140 @@
/*
* Controls.cpp - labeled control widgets
*
* Copyright (c) 2019-2019 Johannes Lorenz <j.git$$$lorenz-ho.me, $$$=@>
*
* 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 "Controls.h"
#include <QLabel>
#include <QString>
#include <QVBoxLayout>
#include "ComboBox.h"
#include "LcdSpinBox.h"
#include "LedCheckbox.h"
#include "Knob.h"
Control::~Control() {}
void KnobControl::setText(const QString &text) { m_knob->setLabel(text); }
QWidget *KnobControl::topWidget() { return m_knob; }
void KnobControl::setModel(AutomatableModel *model)
{
m_knob->setModel(model->dynamicCast<FloatModel>(true));
}
FloatModel *KnobControl::model() { return m_knob->model(); }
AutomatableModelView* KnobControl::modelView() { return m_knob; }
KnobControl::KnobControl(QWidget *parent) :
m_knob(new Knob(parent)) {}
KnobControl::~KnobControl() {}
void ComboControl::setText(const QString &text) { m_label->setText(text); }
void ComboControl::setModel(AutomatableModel *model)
{
m_combo->setModel(model->dynamicCast<ComboBoxModel>(true));
}
ComboBoxModel *ComboControl::model() { return m_combo->model(); }
AutomatableModelView* ComboControl::modelView() { return m_combo; }
ComboControl::ComboControl(QWidget *parent) :
m_widget(new QWidget(parent)),
m_combo(new ComboBox(nullptr)),
m_label(new QLabel(m_widget))
{
m_combo->setFixedSize(64, 22);
QVBoxLayout* vbox = new QVBoxLayout(m_widget);
vbox->addWidget(m_combo);
vbox->addWidget(m_label);
m_combo->repaint();
}
ComboControl::~ComboControl() {}
void CheckControl::setText(const QString &text) { m_label->setText(text); }
QWidget *CheckControl::topWidget() { return m_widget; }
void CheckControl::setModel(AutomatableModel *model)
{
m_checkBox->setModel(model->dynamicCast<BoolModel>(true));
}
BoolModel *CheckControl::model() { return m_checkBox->model(); }
AutomatableModelView* CheckControl::modelView() { return m_checkBox; }
CheckControl::CheckControl(QWidget *parent) :
m_widget(new QWidget(parent)),
m_checkBox(new LedCheckBox(nullptr, QString(), LedCheckBox::Green)),
m_label(new QLabel(m_widget))
{
QVBoxLayout* vbox = new QVBoxLayout(m_widget);
vbox->addWidget(m_checkBox);
vbox->addWidget(m_label);
}
CheckControl::~CheckControl() {}
void LcdControl::setText(const QString &text) { m_lcd->setLabel(text); }
QWidget *LcdControl::topWidget() { return m_lcd; }
void LcdControl::setModel(AutomatableModel *model)
{
m_lcd->setModel(model->dynamicCast<IntModel>(true));
}
IntModel *LcdControl::model() { return m_lcd->model(); }
AutomatableModelView* LcdControl::modelView() { return m_lcd; }
LcdControl::LcdControl(int numDigits, QWidget *parent) :
m_lcd(new LcdSpinBox(numDigits, parent))
{
}
LcdControl::~LcdControl() {}

View File

@@ -0,0 +1,160 @@
/*
* LinkedModelGroupViews.h - view for groups of linkable models
*
* Copyright (c) 2019-2019 Johannes Lorenz <j.git$$$lorenz-ho.me, $$$=@>
*
* 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 "LinkedModelGroupViews.h"
#include <QPushButton>
#include "Controls.h"
#include "ControlLayout.h"
#include "LinkedModelGroups.h"
/*
LinkedModelGroupViewBase
*/
LinkedModelGroupView::LinkedModelGroupView(QWidget* parent,
LinkedModelGroup *model, std::size_t colNum) :
QWidget(parent),
m_model(model),
m_colNum(colNum),
m_layout(new ControlLayout(this))
{
}
LinkedModelGroupView::~LinkedModelGroupView() {}
void LinkedModelGroupView::modelChanged(LinkedModelGroup *group)
{
// reconnect models
group->foreach_model([this](const std::string& str,
const LinkedModelGroup::ModelInfo& minf)
{
auto itr = m_widgets.find(str);
// in case there are new or deleted widgets, the subclass has already
// modified m_widgets, so this will go into the else case
if (itr == m_widgets.end())
{
// no widget? this can happen when the whole view is being destroyed
// (for some strange reasons)
}
else
{
itr->second->setModel(minf.m_model);
}
});
m_model = group;
}
void LinkedModelGroupView::addControl(Control* ctrl, const std::string& id,
const std::string &display, bool removable)
{
int wdgNum = static_cast<int>(m_widgets.size());
if (ctrl)
{
QWidget* box = new QWidget(this);
QHBoxLayout* boxLayout = new QHBoxLayout(box);
boxLayout->addWidget(ctrl->topWidget());
if (removable)
{
QPushButton* removeBtn = new QPushButton;
removeBtn->setIcon( embed::getIconPixmap( "discard" ) );
QObject::connect(removeBtn, &QPushButton::clicked,
this, [this,ctrl](bool){
AutomatableModel* controlModel = ctrl->model();
// remove control out of model group
// (will also remove it from the UI)
m_model->removeControl(controlModel);
// delete model (includes disconnecting all connections)
delete controlModel;
},
Qt::DirectConnection);
boxLayout->addWidget(removeBtn);
}
// required, so the Layout knows how to sort/filter widgets by string
box->setObjectName(QString::fromStdString(display));
m_layout->addWidget(box);
// take ownership of control and add it
m_widgets.emplace(id, std::unique_ptr<Control>(ctrl));
++wdgNum;
}
if (isHidden()) { setHidden(false); }
}
void LinkedModelGroupView::removeControl(const QString& key)
{
auto itr = m_widgets.find(key.toStdString());
if (itr != m_widgets.end())
{
QLayoutItem* item = m_layout->itemByString(key);
Q_ASSERT(!!item);
QWidget* wdg = item->widget();
Q_ASSERT(!!wdg);
// remove item from layout
m_layout->removeItem(item);
// the widget still exists and is visible - remove it now
delete wdg;
// erase widget pointer from dictionary
m_widgets.erase(itr);
// repaint immediately, so we don't have dangling model views
m_layout->update();
}
}
/*
LinkedModelGroupsViewBase
*/
void LinkedModelGroupsView::modelChanged(LinkedModelGroups *groups)
{
LinkedModelGroupView* groupView = getGroupView();
LinkedModelGroup* group0 = groups->getGroup(0);
if (group0 && groupView)
{
groupView->modelChanged(group0);
}
}