diff options
-rw-r--r-- | AUTHORS | 5 | ||||
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | src/jalv_qt4.cpp | 571 |
3 files changed, 564 insertions, 15 deletions
@@ -8,4 +8,7 @@ Various UI improvements: Robin Gareus <robin@gareus.org> Preset menu support for Qt: - Timo Westkämper <timo.westkamper@gmail.com>
\ No newline at end of file + Timo Westkämper <timo.westkamper@gmail.com> + +Qt4 Generic Plugin UI: + Amadeus Folego <amadeusfolego@gmail.com> @@ -1,6 +1,7 @@ jalv (1.4.7) unstable; * Improve preset support + * Add generic Qt control UI from Amadeus Folego * Set Jack port order metadata * Add command prompt to console version for changing controls * Exit on Jack shutdown (Patch from Robin Gareus) @@ -9,7 +10,7 @@ jalv (1.4.7) unstable; * Fix semaphore correctness issues * Use moc-qt4 if present for systems with multiple Qt versions - -- David Robillard <d@drobilla.net> Sat, 07 Mar 2015 03:44:18 -0500 + -- David Robillard <d@drobilla.net> Tue, 07 Apr 2015 17:30:32 -0400 jalv (1.4.6) stable; diff --git a/src/jalv_qt4.cpp b/src/jalv_qt4.cpp index 1f227f9..38dc3cf 100644 --- a/src/jalv_qt4.cpp +++ b/src/jalv_qt4.cpp @@ -14,18 +14,228 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <stdio.h> +#include <math.h> + #include "jalv_internal.h" -#include <QApplication> -#include <QMainWindow> -#include <QMenuBar> -#include <QPushButton> -#include <QTimer> +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/port-props/port-props.h" + +#include <QtGui> + +#define CONTROL_WIDTH 150 +#define DIAL_STEPS 10000 static QApplication* app = NULL; -class PresetAction : public QAction { +class FlowLayout : public QLayout +{ +public: + FlowLayout(QWidget* parent, + int margin = -1, + int hSpacing = -1, + int vSpacing = -1); + + FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); + + ~FlowLayout(); + + void addItem(QLayoutItem* item); + int horizontalSpacing() const; + int verticalSpacing() const; + Qt::Orientations expandingDirections() const; + bool hasHeightForWidth() const; + int heightForWidth(int) const; + int count() const; + QLayoutItem* itemAt(int index) const; + QSize minimumSize() const; + void setGeometry(const QRect &rect); + QSize sizeHint() const; + QLayoutItem* takeAt(int index); + +private: + int doLayout(const QRect &rect, bool testOnly) const; + int smartSpacing(QStyle::PixelMetric pm) const; + + QList<QLayoutItem*> itemList; + int m_hSpace; + int m_vSpace; +}; + +FlowLayout::FlowLayout(QWidget* parent, int margin, int hSpacing, int vSpacing) + : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing) +{ + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) + : m_hSpace(hSpacing), m_vSpace(vSpacing) +{ + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::~FlowLayout() +{ + QLayoutItem* item; + while ((item = takeAt(0))) { + delete item; + } +} + +void +FlowLayout::addItem(QLayoutItem* item) +{ + itemList.append(item); +} + +int +FlowLayout::horizontalSpacing() const +{ + if (m_hSpace >= 0) { + return m_hSpace; + } else { + return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); + } +} + +int +FlowLayout::verticalSpacing() const +{ + if (m_vSpace >= 0) { + return m_vSpace; + } else { + return smartSpacing(QStyle::PM_LayoutVerticalSpacing); + } +} + +int +FlowLayout::count() const +{ + return itemList.size(); +} + +QLayoutItem* +FlowLayout::itemAt(int index) const +{ + return itemList.value(index); +} + +QLayoutItem* +FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < itemList.size()) { + return itemList.takeAt(index); + } else { + return 0; + } +} + +Qt::Orientations +FlowLayout::expandingDirections() const +{ + return 0; +} + +bool +FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int +FlowLayout::heightForWidth(int width) const +{ + return doLayout(QRect(0, 0, width, 0), true); +} + +void +FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + doLayout(rect, false); +} + +QSize +FlowLayout::sizeHint() const +{ + return minimumSize(); +} + +QSize +FlowLayout::minimumSize() const +{ + QSize size; + QLayoutItem* item; + foreach (item, itemList) { + size = size.expandedTo(item->minimumSize()); + } + + return size + QSize(2 * margin(), 2 * margin()); +} + +int +FlowLayout::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; + + QLayoutItem* item; + foreach (item, itemList) { + QWidget* wid = item->widget(); + 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()); + } + return y + lineHeight - rect.y() + bottom; +} + +int +FlowLayout::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, 0, pw); + } else { + return static_cast<QLayout*>(parent)->spacing(); + } +} + +class PresetAction : public QAction +{ Q_OBJECT public: @@ -47,6 +257,45 @@ private: LilvNode* _preset; }; +typedef struct { + Jalv* jalv; + struct Port* port; +} PortContainer; + +class Control : public QGroupBox +{ + Q_OBJECT + +public: + Control(PortContainer portContainer, QWidget* parent = 0); + + Q_SLOT void dialChanged(int value); + + void setValue(float value); + + QDial* dial; + +private: + void setRange(float min, float max); + QString getValueLabel(float value); + float getValue(); + + const LilvPlugin* plugin; + struct Port* port; + + QLabel* label; + QString name; + int steps; + float max; + float min; + bool isInteger; + bool isEnum; + bool isLogarithmic; + + std::vector<float> scalePoints; + std::map<float, const char*> scaleMap; +}; + #include "jalv_qt4_meta.hpp" extern "C" { @@ -55,6 +304,8 @@ int jalv_init(int* argc, char*** argv, JalvOptions* opts) { app = new QApplication(*argc, *argv, true); + app->setStyleSheet("QGroupBox::title { subcontrol-position: top center }"); + return 0; } @@ -83,9 +334,14 @@ jalv_ui_port_event(Jalv* jalv, uint32_t protocol, const void* buffer) { + Control* control = (Control*)jalv->ports[port_index].widget; + if (control) { + control->setValue(*(const float*)buffer); + } } -class Timer : public QTimer { +class Timer : public QTimer +{ public: explicit Timer(Jalv* jalv) : _jalv(jalv) {} @@ -112,6 +368,284 @@ add_preset_to_menu(Jalv* jalv, return 0; } +Control::Control(PortContainer portContainer, QWidget* parent) + : QGroupBox(parent) + , dial(new QDial()) + , plugin(portContainer.jalv->plugin) + , port(portContainer.port) + , label(new QLabel()) +{ + const LilvPort* lilvPort = port->lilv_port; + LilvWorld* world = portContainer.jalv->world; + + LilvNode* lv2_integer = lilv_new_uri(world, LV2_CORE__integer); + LilvNode* lv2_toggled = lilv_new_uri(world, LV2_CORE__toggled); + LilvNode* lv2_enumeration = lilv_new_uri(world, LV2_CORE__enumeration); + LilvNode* logarithmic = lilv_new_uri(world, LV2_PORT_PROPS__logarithmic); + LilvNode* rangeSteps = lilv_new_uri(world, LV2_PORT_PROPS__rangeSteps); + LilvNode* rdfs_comment = lilv_new_uri(world, LILV_NS_RDFS "comment"); + + LilvNode* nmin; + LilvNode* nmax; + LilvNode* ndef; + lilv_port_get_range(plugin, lilvPort, &ndef, &nmin, &nmax); + + if (lilv_port_has_property(plugin, lilvPort, rangeSteps)) { + steps = lilv_node_as_int(rangeSteps); + } else { + steps = DIAL_STEPS; + } + + // Fill scalePoints Map + LilvScalePoints* sp = lilv_port_get_scale_points(plugin, lilvPort); + if (sp) { + LILV_FOREACH(scale_points, s, sp) { + const LilvScalePoint* p = lilv_scale_points_get(sp, s); + const LilvNode* val = lilv_scale_point_get_value(p); + if (!lilv_node_is_float(val)) { + continue; + } + + const float f = lilv_node_as_float(val); + scalePoints.push_back(f); + scaleMap[f] = lilv_node_as_string(lilv_scale_point_get_label(p)); + } + + lilv_scale_points_free(sp); + } + + // Check port properties + isLogarithmic = lilv_port_has_property(plugin, lilvPort, logarithmic); + isInteger = lilv_port_has_property(plugin, lilvPort, lv2_integer); + isEnum = lilv_port_has_property(plugin, lilvPort, lv2_enumeration); + + if (lilv_port_has_property(plugin, lilvPort, lv2_toggled)) { + isInteger = true; + + if (!scaleMap[0]) { + scaleMap[0] = "Off"; + } + if (!scaleMap[1]) { + scaleMap[1] = "On" ; + } + } + + // Find and set min, max and default values for port + float defaultValue = ndef ? lilv_node_as_float(ndef) : port->control; + setRange(lilv_node_as_float(nmin), lilv_node_as_float(nmax)); + setValue(defaultValue); + + // Fill layout + QVBoxLayout* layout = new QVBoxLayout(); + layout->addWidget(label, 0, Qt::AlignHCenter); + layout->addWidget(dial, 0, Qt::AlignHCenter); + setLayout(layout); + + setMinimumWidth(CONTROL_WIDTH); + setMaximumWidth(CONTROL_WIDTH); + + LilvNode* nname = lilv_port_get_name(plugin, lilvPort); + name = QString("%1").arg(lilv_node_as_string(nname)); + + // Handle long names + if (fontMetrics().width(name) > CONTROL_WIDTH) { + setTitle(fontMetrics().elidedText(name, Qt::ElideRight, CONTROL_WIDTH)); + } else { + setTitle(name); + } + + // Set tooltip if comment is available + LilvNode* comment = lilv_port_get(plugin, lilvPort, rdfs_comment); + if (comment) { + QString* tooltip = new QString(); + tooltip->append(lilv_node_as_string(comment)); + setToolTip(*tooltip); + } + + setFlat(true); + + connect(dial, SIGNAL(valueChanged(int)), this, SLOT(dialChanged(int))); + + lilv_node_free(nmin); + lilv_node_free(nmax); + lilv_node_free(ndef); + lilv_node_free(nname); + lilv_node_free(lv2_integer); + lilv_node_free(lv2_toggled); + lilv_node_free(lv2_enumeration); + lilv_node_free(logarithmic); + lilv_node_free(rangeSteps); + lilv_node_free(comment); +} + +void +Control::setValue(float value) +{ + float step; + + if (isInteger) { + step = value; + } else if (isEnum) { + step = (std::find(scalePoints.begin(), scalePoints.end(), value) + - scalePoints.begin()); + } else if (isLogarithmic) { + step = steps * log(value / min) / log(max / min); + } else { + step = value * steps; + } + + dial->setValue(step); + label->setText(getValueLabel(value)); +} + +QString +Control::getValueLabel(float value) +{ + if (scaleMap[value]) { + if (fontMetrics().width(scaleMap[value]) > CONTROL_WIDTH) { + label->setToolTip(scaleMap[value]); + return fontMetrics().elidedText(QString(scaleMap[value]), + Qt::ElideRight, + CONTROL_WIDTH); + } + return scaleMap[value]; + } + + return QString("%1").arg(value); +} + +void +Control::setRange(float minRange, float maxRange) +{ + min = minRange; + max = maxRange; + + if (isLogarithmic) { + minRange = 1; + maxRange = steps; + } else if (isEnum) { + minRange = 0; + maxRange = scalePoints.size() - 1; + } else if (!isInteger) { + minRange *= steps; + maxRange *= steps; + } + + dial->setRange(minRange, maxRange); +} + +float +Control::getValue() +{ + if (isEnum) { + return scalePoints[dial->value()]; + } else if (isInteger) { + return dial->value(); + } else if (isLogarithmic) { + return min * pow(max / min, (float)dial->value() / steps); + } else { + return (float)dial->value() / steps; + } +} + +void +Control::dialChanged(int dialValue) +{ + float value = getValue(); + + label->setText(getValueLabel(value)); + port->control = value; +} + +static bool +portGroupLessThan(const PortContainer &p1, const PortContainer &p2) +{ + Jalv* jalv = p1.jalv; + const LilvPort* port1 = p1.port->lilv_port; + const LilvPort* port2 = p2.port->lilv_port; + + LilvNode* group1 = lilv_port_get( + jalv->plugin, port1, jalv->nodes.pg_group); + LilvNode* group2 = lilv_port_get( + jalv->plugin, port2, jalv->nodes.pg_group); + + const int cmp = (group1 && group2) + ? strcmp(lilv_node_as_string(group1), lilv_node_as_string(group2)) + : ((intptr_t)group1 - (intptr_t)group2); + + lilv_node_free(group2); + lilv_node_free(group1); + + return cmp < 0; +} + +static QWidget* +build_control_widget(Jalv* jalv) +{ + const LilvPlugin* plugin = jalv->plugin; + LilvWorld* world = jalv->world; + + LilvNode* pprop_notOnGUI = lilv_new_uri(world, LV2_PORT_PROPS__notOnGUI); + + QList<PortContainer> portContainers; + for (unsigned i = 0; i < jalv->num_ports; ++i) { + if (!jalv->opts.show_hidden && + lilv_port_has_property(plugin, jalv->ports[i].lilv_port, pprop_notOnGUI)) { + continue; + } + + if (jalv->ports[i].type == TYPE_CONTROL) { + PortContainer portContainer; + portContainer.jalv = jalv; + portContainer.port = &jalv->ports[i]; + portContainers.append(portContainer); + } + } + + qSort(portContainers.begin(), portContainers.end(), portGroupLessThan); + + QWidget* grid = new QWidget(); + FlowLayout* flowLayout = new FlowLayout(); + QLayout* layout = flowLayout; + + LilvNode* lastGroup = NULL; + QHBoxLayout* groupLayout; + for (int i = 0; i < portContainers.count(); ++i) { + PortContainer portContainer = portContainers[i]; + Port* port = portContainer.port; + + Control* control = new Control(portContainer); + LilvNode* group = lilv_port_get( + plugin, port->lilv_port, jalv->nodes.pg_group); + if (group) { + if (!lilv_node_equals(group, lastGroup)) { + /* Group has changed */ + LilvNode* groupName = lilv_world_get( + world, group, jalv->nodes.lv2_name, NULL); + QGroupBox* groupBox = new QGroupBox(lilv_node_as_string(groupName)); + + groupLayout = new QHBoxLayout(); + groupBox->setLayout(groupLayout); + layout->addWidget(groupBox); + } + + groupLayout->addWidget(control); + } else { + layout->addWidget(control); + } + lastGroup = group; + + uint32_t index = lilv_port_get_index(plugin, port->lilv_port); + jalv->ports[index].widget = control; + } + + grid->setLayout(layout); + + lilv_node_free(pprop_notOnGUI); + + return grid; +} + int jalv_open_ui(Jalv* jalv) { @@ -128,19 +662,30 @@ jalv_open_ui(Jalv* jalv) jalv_load_presets(jalv, add_preset_to_menu, presets_menu); - if (jalv->ui) { + if (jalv->ui && !jalv->opts.generic_ui) { jalv_ui_instantiate(jalv, jalv_native_ui_type(jalv), win); } + QWidget* widget; if (jalv->ui_instance) { - QWidget* widget = (QWidget*)suil_instance_get_widget(jalv->ui_instance); - win->setCentralWidget(widget); + widget = (QWidget*)suil_instance_get_widget(jalv->ui_instance); } else { - QPushButton* button = new QPushButton("Close"); - win->setCentralWidget(button); - QObject::connect(button, SIGNAL(clicked()), app, SLOT(quit())); + QWidget* controlWidget = build_control_widget(jalv); + + widget = new QScrollArea(); + ((QScrollArea*)widget)->setWidget(controlWidget); + ((QScrollArea*)widget)->setWidgetResizable(true); + widget->setMinimumWidth(800); + widget->setMinimumHeight(600); } + + LilvNode* name = lilv_plugin_get_name(jalv->plugin); + win->setWindowTitle(lilv_node_as_string(name)); + lilv_node_free(name); + + win->setCentralWidget(widget); app->connect(app, SIGNAL(lastWindowClosed()), app, SLOT(quit())); + win->show(); Timer* timer = new Timer(jalv); |