From eebdc0f4be2be800d86758e91cabb47d55d6ac75 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> Date: Fri, 21 Feb 2020 19:26:29 +0100 Subject: [PATCH] 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. --- include/ControlLayout.h | 133 ++++++++++ include/Controls.h | 134 ++++++++++ include/LinkedModelGroupViews.h | 105 ++++++++ include/LinkedModelGroups.h | 175 ++++++++++++ include/stdshims.h | 7 + src/core/CMakeLists.txt | 1 + src/core/LinkedModelGroups.cpp | 185 +++++++++++++ src/gui/CMakeLists.txt | 3 + src/gui/widgets/ControlLayout.cpp | 308 ++++++++++++++++++++++ src/gui/widgets/Controls.cpp | 140 ++++++++++ src/gui/widgets/LinkedModelGroupViews.cpp | 160 +++++++++++ 11 files changed, 1351 insertions(+) create mode 100644 include/ControlLayout.h create mode 100644 include/Controls.h create mode 100644 include/LinkedModelGroupViews.h create mode 100644 include/LinkedModelGroups.h create mode 100644 src/core/LinkedModelGroups.cpp create mode 100644 src/gui/widgets/ControlLayout.cpp create mode 100644 src/gui/widgets/Controls.cpp create mode 100644 src/gui/widgets/LinkedModelGroupViews.cpp diff --git a/include/ControlLayout.h b/include/ControlLayout.h new file mode 100644 index 00000000000..625b3f4681d --- /dev/null +++ b/include/ControlLayout.h @@ -0,0 +1,133 @@ +/* + * ControlLayout.h - layout for controls + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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$ +** +****************************************************************************/ + +#ifndef CONTROLLAYOUT_H +#define CONTROLLAYOUT_H + +#include +#include +#include +class QLayoutItem; +class QRect; +class QString; + +/** + Layout for controls (models) + + Originally token from Qt's FlowLayout example. Modified. + + Features a search bar, as well as looking up widgets with string keys + Keys have to be provided in the widgets' objectNames +*/ +class ControlLayout : public QLayout +{ + Q_OBJECT + +public: + explicit ControlLayout(QWidget *parent, + int margin = -1, int hSpacing = -1, int vSpacing = -1); + ~ControlLayout() override; + + void addItem(QLayoutItem *item) override; + int horizontalSpacing() const; + int verticalSpacing() const; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int) const override; + int count() const override; + QLayoutItem *itemAt(int index) const override; + QLayoutItem *itemByString(const QString& key) const; + QSize minimumSize() const override; + void setGeometry(const QRect &rect) override; + QSize sizeHint() const override; + QLayoutItem *takeAt(int index) override; + +private slots: + void onTextChanged(const QString&); + +private: + int doLayout(const QRect &rect, bool testOnly) const; + int smartSpacing(QStyle::PixelMetric pm) const; + QMap::const_iterator pairAt(int index) const; + + QMultiMap m_itemMap; + int m_hSpace; + int m_vSpace; + // relevant dimension is width, as later, heightForWidth() will be called + // 400 looks good and is ~4 knobs in a row + constexpr const static int m_minWidth = 400; + class QLineEdit* m_searchBar; + //! name of search bar, must be ASCII sorted before any alpha numerics + static constexpr const char* s_searchBarName = "!!searchBar!!"; +}; + +#endif // CONTROLLAYOUT_H diff --git a/include/Controls.h b/include/Controls.h new file mode 100644 index 00000000000..236abbc1199 --- /dev/null +++ b/include/Controls.h @@ -0,0 +1,134 @@ +/* + * Controls.h - labeled control widgets + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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. + * + */ + +#ifndef CONTROLS_H +#define CONTROLS_H + + +#include "Model.h" + +// headers only required for covariance +#include "AutomatableModel.h" +#include "ComboBoxModel.h" + + +class QString; +class QWidget; +class AutomatableModel; + + +/** + These classes provide + - a control with a text label + - a type safe way to set a model + (justification: setting the wrong typed model to a widget will cause + hard-to-find runtime errors) +*/ +class Control +{ +public: + virtual QWidget* topWidget() = 0; + virtual void setText(const QString& text) = 0; + + virtual void setModel(AutomatableModel* model) = 0; + virtual AutomatableModel* model() = 0; + virtual class AutomatableModelView* modelView() = 0; + + virtual ~Control(); +}; + + +class KnobControl : public Control +{ + class Knob* m_knob; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + FloatModel* model() override; + class AutomatableModelView* modelView() override; + + KnobControl(QWidget* parent = nullptr); + ~KnobControl() override; +}; + + +class ComboControl : public Control +{ + QWidget* m_widget; + class ComboBox* m_combo; + class QLabel* m_label; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override { return m_widget; } + + void setModel(AutomatableModel* model) override; + ComboBoxModel* model() override; + class AutomatableModelView* modelView() override; + + ComboControl(QWidget* parent = nullptr); + ~ComboControl() override; +}; + + +class LcdControl : public Control +{ + class LcdSpinBox* m_lcd; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + IntModel* model() override; + class AutomatableModelView* modelView() override; + + LcdControl(int numDigits, QWidget* parent = nullptr); + ~LcdControl() override; +}; + + +class CheckControl : public Control +{ + QWidget* m_widget; + class LedCheckBox* m_checkBox; + QLabel* m_label; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + BoolModel *model() override; + class AutomatableModelView* modelView() override; + + CheckControl(QWidget* parent = nullptr); + ~CheckControl() override; +}; + + +#endif // CONTROLS_H diff --git a/include/LinkedModelGroupViews.h b/include/LinkedModelGroupViews.h new file mode 100644 index 00000000000..79fab76afd5 --- /dev/null +++ b/include/LinkedModelGroupViews.h @@ -0,0 +1,105 @@ +/* + * LinkedModelGroupViews.h - view for groups of linkable models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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. + * + */ + +#ifndef LINKEDMODELGROUPVIEWS_H +#define LINKEDMODELGROUPVIEWS_H + + +#include +#include +#include +#include + + +/** + @file LinkedModelGroupViews.h + See Lv2ViewBase.h for example usage +*/ + + +/** + View for a representative processor + + Features: + * Remove button for removable models + * Simple handling of adding, removing and model changing + + @note Neither this class, nor any inheriting classes, shall inherit + ModelView. The "view" in the name is just for consistency + with LinkedModelGroupsView. +*/ +class LinkedModelGroupView : public QWidget +{ +public: + /** + @param colNum numbers of columns for the controls + (link LEDs not counted) + */ + LinkedModelGroupView(QWidget *parent, class LinkedModelGroup* model, + std::size_t colNum); + ~LinkedModelGroupView(); + + //! Reconnect models if model changed + void modelChanged(class LinkedModelGroup *linkedModelGroup); + +protected: + //! Add a control to this widget + //! @warning This widget will own this control, do not free it + void addControl(class Control *ctrl, const std::string &id, + const std::string& display, bool removable); + + void removeControl(const QString &key); + +private: + class LinkedModelGroup* m_model; + + //! column number in surrounding grid in LinkedModelGroupsView + std::size_t m_colNum; + class ControlLayout* m_layout; + std::map> m_widgets; +}; + + +/** + Container class for one LinkedModelGroupView + + @note It's intended this class does not inherit from ModelView. + Inheriting classes need to do that, see e.g. Lv2Instrument.h +*/ +class LinkedModelGroupsView +{ +protected: + ~LinkedModelGroupsView() = default; + + //! Reconnect models if model changed; to be called by child virtuals + void modelChanged(class LinkedModelGroups* ctrlBase); + +private: + //! The base class must return the adressed group view, + //! which has the same value as "this" + virtual LinkedModelGroupView* getGroupView() = 0; +}; + + +#endif // LINKEDMODELGROUPVIEWS_H diff --git a/include/LinkedModelGroups.h b/include/LinkedModelGroups.h new file mode 100644 index 00000000000..355290d9aee --- /dev/null +++ b/include/LinkedModelGroups.h @@ -0,0 +1,175 @@ +/* + * LinkedModelGroups.h - base classes for groups of linked models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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. + * + */ + +#ifndef LINKEDMODELGROUPS_H +#define LINKEDMODELGROUPS_H + + +#include +#include +#include + +#include "Model.h" + + +/** + @file LinkedModelGroups.h + See Lv2ControlBase.h and Lv2Proc.h for example usage +*/ + + +/** + Base class for a group of linked models + + See the LinkedModelGroup class for explenations + + Features: + * Models are stored by their QObject::objectName + * Models are linked automatically +*/ +class LinkedModelGroup : public Model +{ + Q_OBJECT +public: + /* + Initialization + */ + //! @param parent model of the LinkedModelGroups class + LinkedModelGroup(Model* parent) : Model(parent) {} + + /* + Linking (initially only) + */ + void linkControls(LinkedModelGroup *other); + + /* + Models + */ + struct ModelInfo + { + QString m_name; + class AutomatableModel* m_model; + ModelInfo() { /* hopefully no one will use this */ } // TODO: remove? + ModelInfo(const QString& name, AutomatableModel* model) + : m_name(name), m_model(model) {} + }; + + // TODO: refactor those 2 + template + void foreach_model(const Functor& ftor) + { + for (auto itr = m_models.begin(); itr != m_models.end(); ++itr) + { + ftor(itr->first, itr->second); + } + } + + template + void foreach_model(const Functor& ftor) const + { + for (auto itr = m_models.cbegin(); itr != m_models.cend(); ++itr) + { + ftor(itr->first, itr->second); + } + } + + std::size_t modelNum() const { return m_models.size(); } + bool containsModel(const QString& name) const; + void removeControl(AutomatableModel *); + + /* + Load/Save + */ + void saveValues(class QDomDocument& doc, class QDomElement& that); + void loadValues(const class QDomElement& that); + +signals: + // NOTE: when separating core from UI, this will need to be removed + // (who would kno if the client is Qt, i.e. it may not have slots at all) + // In this case you'd e.g. send the UI something like + // "/added " + void modelAdded(AutomatableModel* added); + void modelRemoved(AutomatableModel* removed); + +public: + AutomatableModel* getModel(const std::string& s) + { + auto itr = m_models.find(s); + return (itr == m_models.end()) ? nullptr : itr->second.m_model; + } + + //! Register a further model + void addModel(class AutomatableModel* model, const QString& name); + //! Unregister a model, return true if a model was erased + bool eraseModel(const QString& name); + + //! Remove all models + void clearModels(); + +private: + //! models for the controls + std::map m_models; +}; + + +/** + Container for a group of linked models + + Each group contains the same models and model types. The models are + numbered, and equal numbered models are associated and always linked. + + A typical application are two mono plugins making a stereo plugin. + + @note Though this class can contain multiple model groups, a corresponding + view ("LinkedModelGroupViews") will only display one group, as they all have + the same values + + @note Though called "container", this class does not contain, but only + know the single groups. The inheriting classes are responsible for storage. +*/ +class LinkedModelGroups +{ +public: + virtual ~LinkedModelGroups(); + + void linkAllModels(); + + /* + Load/Save + */ + void saveSettings(class QDomDocument& doc, class QDomElement& that); + void loadSettings(const class QDomElement& that); + + /* + General + */ + //! Derived classes must return the group with index @p idx, + //! or nullptr if @p is out of range + virtual LinkedModelGroup* getGroup(std::size_t idx) = 0; + //! @see getGroup + virtual const LinkedModelGroup* getGroup(std::size_t idx) const = 0; +}; + + +#endif // LINKEDMODELGROUPS_H diff --git a/include/stdshims.h b/include/stdshims.h index 85c4f457aab..5eee6543cac 100644 --- a/include/stdshims.h +++ b/include/stdshims.h @@ -21,6 +21,13 @@ std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } + +//! Overload for the case a deleter should be specified +template +std::unique_ptr make_unique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} #endif #endif // include guard diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a50b32a0ff2..f1c183e3f55 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/LinkedModelGroups.cpp b/src/core/LinkedModelGroups.cpp new file mode 100644 index 00000000000..c9bbc475a8f --- /dev/null +++ b/src/core/LinkedModelGroups.cpp @@ -0,0 +1,185 @@ +/* + * LinkedModelGroups.cpp - base classes for groups of linked models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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 +#include + +#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); + } +} + diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index af316fddb01..dbdcc5d84da 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/widgets/ControlLayout.cpp b/src/gui/widgets/ControlLayout.cpp new file mode 100644 index 00000000000..afd4e68e42e --- /dev/null +++ b/src/gui/widgets/ControlLayout.cpp @@ -0,0 +1,308 @@ +/* + * ControlLayout.cpp - implementation for ControlLayout.h + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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 +#include +#include +#include +#include + +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::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::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 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(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } + else { return static_cast(parent)->spacing(); } +} + + diff --git a/src/gui/widgets/Controls.cpp b/src/gui/widgets/Controls.cpp new file mode 100644 index 00000000000..15b4e0d282a --- /dev/null +++ b/src/gui/widgets/Controls.cpp @@ -0,0 +1,140 @@ +/* + * Controls.cpp - labeled control widgets + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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 +#include +#include + +#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(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(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(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(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() {} + diff --git a/src/gui/widgets/LinkedModelGroupViews.cpp b/src/gui/widgets/LinkedModelGroupViews.cpp new file mode 100644 index 00000000000..21d40efcc1e --- /dev/null +++ b/src/gui/widgets/LinkedModelGroupViews.cpp @@ -0,0 +1,160 @@ +/* + * LinkedModelGroupViews.h - view for groups of linkable models + * + * Copyright (c) 2019-2019 Johannes Lorenz + * + * 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 +#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(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(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); + } +} + +