diff options
Diffstat (limited to 'src/jalv_qt.cpp')
-rw-r--r-- | src/jalv_qt.cpp | 730 |
1 files changed, 730 insertions, 0 deletions
diff --git a/src/jalv_qt.cpp b/src/jalv_qt.cpp new file mode 100644 index 0000000..e9d2121 --- /dev/null +++ b/src/jalv_qt.cpp @@ -0,0 +1,730 @@ +/* + Copyright 2007-2016 David Robillard <http://drobilla.net> + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include <algorithm> +#include <cmath> +#include <cstdio> + +#include "jalv_internal.h" + +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/port-props/port-props.h" + +#include <qglobal.h> + +#if QT_VERSION >= 0x050000 +# include <QAction> +# include <QApplication> +# include <QDial> +# include <QGroupBox> +# include <QLabel> +# include <QLayout> +# include <QMainWindow> +# include <QMenu> +# include <QMenuBar> +# include <QScrollArea> +# include <QStyle> +# include <QTimer> +# include <QWidget> +# include <QWindow> +#else +# include <QtGui> +#endif + +#define CONTROL_WIDTH 150 +#define DIAL_STEPS 10000 + +static QApplication* app = NULL; + +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: + PresetAction(QObject* parent, Jalv* jalv, LilvNode* preset) + : QAction(parent) + , _jalv(jalv) + , _preset(preset) + { + connect(this, SIGNAL(triggered()), + this, SLOT(presetChosen())); + } + + Q_SLOT void presetChosen() { + jalv_apply_preset(_jalv, _preset); + } + +private: + Jalv* _jalv; + 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; +}; + +#if QT_VERSION >= 0x050000 +# include "jalv_qt5_meta.hpp" +#else +# include "jalv_qt4_meta.hpp" +#endif + +extern "C" { + +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; +} + +const char* +jalv_native_ui_type(void) +{ +#if QT_VERSION >= 0x050000 + return "http://lv2plug.in/ns/extensions/ui#Qt5UI"; +#else + return "http://lv2plug.in/ns/extensions/ui#Qt4UI"; +#endif +} + +void +jalv_ui_port_event(Jalv* jalv, + uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer) +{ + if (jalv->ui_instance) { + suil_instance_port_event(jalv->ui_instance, port_index, + buffer_size, protocol, buffer); + } else { + Control* control = (Control*)jalv->ports[port_index].widget; + if (control) { + control->setValue(*(const float*)buffer); + } + } +} + +class Timer : public QTimer +{ +public: + explicit Timer(Jalv* jalv) : _jalv(jalv) {} + + void timerEvent(QTimerEvent* e) { + jalv_update(_jalv); + } + +private: + Jalv* _jalv; +}; + +static int +add_preset_to_menu(Jalv* jalv, + const LilvNode* node, + const LilvNode* title, + void* data) +{ + QMenu* menu = (QMenu*)data; + const char* label = lilv_node_as_string(title); + + QAction* action = new PresetAction(menu, jalv, lilv_node_duplicate(node)); + action->setText(label); + menu->addAction(action); + return 0; +} + +Control::Control(PortContainer portContainer, QWidget* parent) + : QGroupBox(parent) + , dial(new QDial()) + , plugin(portContainer.jalv->plugin) + , port(portContainer.port) + , label(new QLabel()) +{ + JalvNodes* nodes = &portContainer.jalv->nodes; + const LilvPort* lilvPort = port->lilv_port; + + LilvNode* nmin; + LilvNode* nmax; + LilvNode* ndef; + lilv_port_get_range(plugin, lilvPort, &ndef, &nmin, &nmax); + + LilvNode* stepsNode = lilv_port_get(plugin, lilvPort, nodes->pprops_rangeSteps); + if (lilv_node_is_int(stepsNode)) { + steps = std::max(lilv_node_as_int(stepsNode), 2); + } else { + steps = DIAL_STEPS; + } + lilv_node_free(stepsNode); + + // 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) && !lilv_node_is_int(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, nodes->pprops_logarithmic); + isInteger = lilv_port_has_property(plugin, lilvPort, nodes->lv2_integer); + isEnum = lilv_port_has_property(plugin, lilvPort, nodes->lv2_enumeration); + + if (lilv_port_has_property(plugin, lilvPort, nodes->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, nodes->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(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 - 1)); + } 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; + + 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, + jalv->nodes.pprops_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 = NULL; + 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); + } + lilv_node_free(lastGroup); + lastGroup = group; + + uint32_t index = lilv_port_get_index(plugin, port->lilv_port); + jalv->ports[index].widget = control; + } + lilv_node_free(lastGroup); + + grid->setLayout(layout); + + return grid; +} + +bool +jalv_discover_ui(Jalv* jalv) +{ + return true; +} + +int +jalv_open_ui(Jalv* jalv) +{ + QMainWindow* win = new QMainWindow(); + QMenu* file_menu = win->menuBar()->addMenu("&File"); + QMenu* presets_menu = win->menuBar()->addMenu("&Presets"); + QAction* quit_action = new QAction("&Quit", win); + + QObject::connect(quit_action, SIGNAL(triggered()), win, SLOT(close())); + quit_action->setShortcuts(QKeySequence::Quit); + quit_action->setStatusTip("Quit Jalv"); + file_menu->addAction(quit_action); + + jalv_load_presets(jalv, add_preset_to_menu, presets_menu); + + if (jalv->ui && !jalv->opts.generic_ui) { + jalv_ui_instantiate(jalv, jalv_native_ui_type(), win); + } + + QWidget* widget; + if (jalv->ui_instance) { + widget = (QWidget*)suil_instance_get_widget(jalv->ui_instance); + } else { + 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(); + if (jalv->ui_instance && !jalv_ui_is_resizable(jalv)) { + widget->setMinimumSize(widget->width(), widget->height()); + widget->setMaximumSize(widget->width(), widget->height()); + win->adjustSize(); + win->setFixedSize(win->width(), win->height()); + } else { + win->resize(widget->width(), + widget->height() + win->menuBar()->height()); + } + + Timer* timer = new Timer(jalv); + timer->start(1000 / jalv->ui_update_hz); + + int ret = app->exec(); + zix_sem_post(&jalv->done); + return ret; +} + +int +jalv_close_ui(Jalv* jalv) +{ + app->quit(); + return 0; +} + +} // extern "C" |