• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 36213733900

26 Sep 2026 03:05AM UTC coverage: 42.353% (+0.1%) from 42.23%
36213733900

Pull #2310

github

web-flow
Merge 9f9d60cd9 into c0c64ec39
Pull Request #2310: bitfocus: host companion modules the way companion does

41 of 2024 new or added lines in 11 files covered. (2.03%)

210 existing lines in 15 files now uncovered.

126773 of 299323 relevant lines covered (42.35%)

118427.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusProtocolSettingsWidget.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
#include "BitfocusProtocolSettingsWidget.hpp"
4

5
#include "BitfocusContext.hpp"
6
#include "BitfocusProtocolFactory.hpp"
7
#include "BitfocusSpecificSettings.hpp"
8

9
#include <State/Widgets/AddressFragmentLineEdit.hpp>
10

11
#include <Device/Protocol/ProtocolSettingsWidget.hpp>
12

13
#include <score/widgets/ValidationPalette.hpp>
14
#include <score/widgets/MarginLess.hpp>
15

16
#include <ossia/detail/algorithms.hpp>
17
#include <ossia/network/value/value_conversion.hpp>
18

19
#include <ossia-qt/js_utilities.hpp>
20

21
#include <QCheckBox>
22
#include <QComboBox>
23
#include <QDir>
24
#include <QFormLayout>
25
#include <QLabel>
26
#include <QLineEdit>
27
#include <QListWidget>
28
#include <QResizeEvent>
29
#include <QScrollArea>
30
#include <QSpinBox>
31
#include <QVariant>
32

33
#include <wobjectimpl.h>
34

35
namespace Protocols
36
{
37

38
BitfocusProtocolSettingsWidget::BitfocusProtocolSettingsWidget(QWidget* parent)
×
39
    : ProtocolSettingsWidget(parent)
×
40
{
×
41
  m_deviceNameEdit = new State::AddressFragmentLineEdit{this};
×
42
  m_deviceNameEdit->setText("OSCdevice");
×
43
  checkForChanges(m_deviceNameEdit);
×
44

45
  m_rootLayout = new score::MarginLess<QFormLayout>{this};
×
46
  m_rootLayout->addRow(tr("Name"), m_deviceNameEdit);
×
47
  m_rootLayout->addRow(new QLabel{
×
48
      tr("To add support for Bitfocus Companion modules:\n - Go to Settings > "
×
49
         "Package Manager\n - Install the \"Bitfocus Companion Modules\" package.")});
50
  m_hasInitLabel = true;
×
51
  m_rootLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
×
52

53
  m_scroll = new QScrollArea{this};
×
54
  m_scroll->setFrameShape(QFrame::NoFrame);
×
55
  QSizePolicy sz;
×
56
  sz.setHorizontalPolicy(QSizePolicy::Ignored);
×
57
  sz.setVerticalPolicy(QSizePolicy::MinimumExpanding);
×
58
  m_scroll->setSizePolicy(sz);
×
59
  m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
×
60
  m_scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
×
61
  m_scroll->setWidgetResizable(true);
×
62
  m_rootLayout->addRow(m_scroll);
×
63
}
×
64

65
Device::DeviceSettings BitfocusProtocolSettingsWidget::getSettings() const
×
66
{
67
  Device::DeviceSettings s;
×
68
  s.name = m_deviceNameEdit->text();
×
69
  s.protocol = BitfocusProtocolFactory::static_concreteKey();
×
70

71
  BitfocusSpecificSettings osc = m_settings;
×
72

NEW
73
  auto set = [&osc](const QString& id, ossia::value v) {
×
NEW
74
    auto it = ossia::find_if(osc.configuration, [&id](const auto& kv) {
×
UNCOV
75
      return kv.first == id;
×
76
    });
77
    if(it != osc.configuration.end())
×
NEW
78
      it->second = std::move(v);
×
79
    else
NEW
80
      osc.configuration.emplace_back(id, std::move(v));
×
NEW
81
  };
×
82

NEW
83
  const bitfocus::module_data* model = osc.handler ? &osc.handler->model() : nullptr;
×
NEW
84
  if(model && m_fieldsLoaded)
×
85
  {
NEW
86
    for(auto& [k, v] : model->config)
×
NEW
87
      if(k != "product" && !m_widgets.contains(k))
×
NEW
88
        if(ossia::none_of(osc.configuration, [&k](auto& kv) { return kv.first == k; }))
×
NEW
89
          osc.configuration.emplace_back(k, ossia::qt::qt_to_ossia{}(v));
×
NEW
90
    osc.upgradeIndex = model->upgradeIndex;
×
NEW
91
    if(auto keys = osc.handler->secretKeys())
×
NEW
92
      osc.secretKeys = std::vector<QString>(keys->begin(), keys->end());
×
NEW
93
  }
×
94

NEW
95
  for(auto& [id, widg] : m_widgets)
×
96
  {
NEW
97
    if(!widg.getValue)
×
NEW
98
      continue;
×
NEW
99
    auto v = widg.getValue();
×
NEW
100
    if(v == widg.shown)
×
101
    {
NEW
102
      if(!widg.source)
×
NEW
103
        continue;
×
NEW
104
      v = *widg.source;
×
NEW
105
    }
×
NEW
106
    set(id, std::move(v));
×
107
  }
×
108
  s.deviceSpecificSettings = QVariant::fromValue(osc);
×
109

110
  return s;
×
111
}
×
112

113
static void makeValidator(QLineEdit* widg, QString rx)
×
114
{
115
  auto val = new QRegularExpressionValidator;
×
116
  if(rx.startsWith("/^"))
×
117
  {
118
    rx.remove(0, 2);
×
119
  }
×
120
  if(rx.endsWith("$/"))
×
121
  {
122
    rx.resize(rx.size() - 2);
×
123
  }
×
124
  val->setRegularExpression(QRegularExpression(rx));
×
125
  val->setParent(widg);
×
126
  widg->setValidator(val);
×
127

128
  QObject::connect(widg, &QLineEdit::textChanged, [widg](const QString& str) {
×
129
    if(!widg->validator())
×
130
      return;
×
131

132
    QString s = str;
×
133
    int i = 0;
×
134
    score::setInputValidity(
×
135
        *widg, widg->validator()->validate(s, i) == QValidator::State::Acceptable
×
136
                   ? score::InputValidity::Valid
137
                   : score::InputValidity::Invalid);
138
  });
×
139
}
×
140

141
void BitfocusProtocolSettingsWidget::resetFields()
×
142
{
143
  m_widgets.clear();
×
144
  m_fieldsLoaded = false;
×
145
  delete m_subWidget;
×
146
  m_subWidget = new QWidget{this};
×
147
  m_subForm = new score::MarginLess<QVBoxLayout>{m_subWidget};
×
148
  m_scroll->setWidget(m_subWidget);
×
149
}
×
150

NEW
151
static ossia::value toSetting(const bitfocus::module_data::config_field& field, QVariant v)
×
152
{
NEW
153
  return ossia::qt::qt_to_ossia{}(bitfocus::toModuleValue(field, v).toVariant());
×
NEW
154
}
×
155

156
template <typename Spin>
NEW
157
static void setupEmptyNumber(Spin* widg, const bitfocus::module_data::config_field& field)
×
158
{
NEW
159
  if(field.hasDefault())
×
NEW
160
    return;
×
NEW
161
  widg->setMinimum(widg->minimum() - widg->singleStep());
×
NEW
162
  widg->setSpecialValueText(QStringLiteral(" "));
×
NEW
163
}
×
164

165
template <typename Spin>
NEW
166
static bool isEmptyNumber(Spin* widg, const bitfocus::module_data::config_field& field)
×
167
{
NEW
168
  return !field.hasDefault() && widg->value() == widg->minimum();
×
169
}
170

NEW
171
static QString colorToString(const QVariant& v)
×
172
{
NEW
173
  if(v.typeId() == QMetaType::QString)
×
NEW
174
    return v.toString();
×
NEW
175
  return QColor::fromRgb(QRgb(v.toLongLong() & 0xFFFFFF)).name();
×
NEW
176
}
×
177

UNCOV
178
void BitfocusProtocolSettingsWidget::updateFields()
×
179
{
180
  if(!m_settings.handler)
×
181
    return;
×
182

183
  resetFields();
×
184

185
  auto& m = m_settings.handler->model();
×
186
  for(auto& field : m.config_fields)
×
187
  {
188
    // 1. Add the field label
189
    QLabel* lab{};
×
190
    if(!field.label.isEmpty())
×
191
    {
192
      lab = new QLabel{};
×
193
      lab->setWordWrap(true);
×
194
      lab->setTextFormat(Qt::RichText);
×
195
      lab->setText(QString("<b>%1</b>").arg(field.label));
×
NEW
196
      if(!field.tooltip.isEmpty())
×
NEW
197
        lab->setToolTip(field.tooltip);
×
198
      m_subForm->addWidget(lab);
×
199
    }
×
200

201
    // 2. Create the widget proper
202
    if(field.type == "static-text")
×
203
    {
204
      if(auto str = field.value.toString(); !str.isEmpty())
×
205
      {
206
        auto static_text = new QLabel{};
×
207
        static_text->setWordWrap(true);
×
208
        static_text->setTextFormat(Qt::RichText);
×
209
        static_text->setText(str);
×
210

211
        m_subForm->addWidget(static_text);
×
212
        m_widgets[field.id]
×
213
            = widget{.label = lab, .widg = static_text, .getValue = {}, .setValue = {}};
×
214
      }
×
215
    }
×
216
    else if(field.type == "number")
×
217
    {
NEW
218
      const double min = field.min.isValid() ? field.min.toDouble() : -1e9;
×
NEW
219
      const double max = field.max.isValid() ? field.max.toDouble() : 1e9;
×
NEW
220
      if(field.isInteger())
×
221
      {
222
        auto widg = new QSpinBox;
×
NEW
223
        widg->setRange(
×
NEW
224
            std::max(min, (double)std::numeric_limits<int>::lowest()),
×
NEW
225
            std::min(max, (double)std::numeric_limits<int>::max()));
×
NEW
226
        if(field.step.isValid())
×
NEW
227
          widg->setSingleStep(std::max(1, field.step.toInt()));
×
NEW
228
        setupEmptyNumber(widg, field);
×
NEW
229
        widg->setValue(field.hasDefault() ? field.default_value.toInt() : widg->minimum());
×
230
        m_subForm->addWidget(widg);
×
231
        m_widgets[field.id]
×
NEW
232
            = {.label = lab, .widg = widg, .getValue = [widg, field]() -> ossia::value {
×
NEW
233
          if(isEmptyNumber(widg, field))
×
NEW
234
            return std::string{};
×
NEW
235
          return toSetting(field, widg->value());
×
236
        }, .setValue = [widg](ossia::value v) {
×
NEW
237
          if(v.get_type() == ossia::val_type::STRING && ossia::convert<std::string>(v).empty())
×
NEW
238
            widg->setValue(widg->minimum());
×
239
          else
NEW
240
            widg->setValue(ossia::convert<int>(v));
×
241
        }};
×
242
      }
×
243
      else
244
      {
NEW
245
        auto widg = new QDoubleSpinBox;
×
NEW
246
        widg->setRange(min, max);
×
NEW
247
        widg->setDecimals(4);
×
NEW
248
        if(field.step.isValid())
×
NEW
249
          widg->setSingleStep(field.step.toDouble());
×
NEW
250
        setupEmptyNumber(widg, field);
×
NEW
251
        widg->setValue(
×
NEW
252
            field.hasDefault() ? field.default_value.toDouble() : widg->minimum());
×
253
        m_subForm->addWidget(widg);
×
254
        m_widgets[field.id]
×
NEW
255
            = {.label = lab, .widg = widg, .getValue = [widg, field]() -> ossia::value {
×
NEW
256
          if(isEmptyNumber(widg, field))
×
NEW
257
            return std::string{};
×
NEW
258
          return toSetting(field, widg->value());
×
259
        }, .setValue = [widg](ossia::value v) {
×
NEW
260
          if(v.get_type() == ossia::val_type::STRING && ossia::convert<std::string>(v).empty())
×
NEW
261
            widg->setValue(widg->minimum());
×
262
          else
NEW
263
            widg->setValue(ossia::convert<double>(v));
×
UNCOV
264
        }};
×
265
      }
266
    }
×
267
    else if(field.type == "checkbox")
×
268
    {
269
      auto widg = new QCheckBox;
×
270
      widg->setChecked(field.default_value.toBool() == true);
×
271
      m_subForm->addWidget(widg);
×
272
      m_widgets[field.id]
×
273
          = {.label = lab, .widg = widg, .getValue = [widg]() -> ossia::value {
×
274
        return widg->isChecked();
×
275
      }, .setValue = [widg](ossia::value v) {
×
276
        widg->setChecked(ossia::convert<bool>(v));
×
277
      }};
×
278
    }
×
NEW
279
    else if(field.type == "dropdown")
×
280
    {
281
      auto widg = new QComboBox;
×
NEW
282
      widg->setEditable(field.allowCustom);
×
NEW
283
      const auto default_v = field.default_value.toString();
×
UNCOV
284
      for(const auto& choice : field.choices)
×
UNCOV
285
        widg->addItem(choice.label, choice.id);
×
NEW
286
      if(int idx = widg->findData(default_v); idx != -1)
×
NEW
287
        widg->setCurrentIndex(idx);
×
NEW
288
      else if(field.allowCustom)
×
NEW
289
        widg->setEditText(default_v);
×
290
      else
NEW
291
        widg->setCurrentIndex(-1);
×
292

293
      m_subForm->addWidget(widg);
×
294
      m_widgets[field.id]
×
NEW
295
          = {.label = lab, .widg = widg, .getValue = [widg, field]() -> ossia::value {
×
NEW
296
        if(widg->isEditable() && widg->currentText() != widg->currentData().toString()
×
NEW
297
           && widg->findText(widg->currentText()) == -1)
×
NEW
298
          return toSetting(field, widg->currentText());
×
NEW
299
        if(widg->currentIndex() < 0)
×
NEW
300
          return toSetting(field, field.default_value);
×
NEW
301
        return toSetting(field, widg->currentData());
×
302
      }, .setValue = [widg](ossia::value v) {
×
303
        auto id = QString::fromStdString(ossia::convert<std::string>(v));
×
NEW
304
        if(int idx = widg->findData(id); idx != -1)
×
305
          widg->setCurrentIndex(idx);
×
NEW
306
        else if(widg->isEditable())
×
NEW
307
          widg->setEditText(id);
×
NEW
308
      }};
×
NEW
309
    }
×
NEW
310
    else if(field.type == "multidropdown")
×
311
    {
NEW
312
      auto widg = new QListWidget;
×
NEW
313
      QStringList defaults;
×
NEW
314
      for(const auto& v : field.default_json.toArray())
×
NEW
315
        defaults.push_back(v.toVariant().toString());
×
NEW
316
      for(const auto& choice : field.choices)
×
317
      {
NEW
318
        auto item = new QListWidgetItem{choice.label, widg};
×
NEW
319
        item->setData(Qt::UserRole, choice.id);
×
NEW
320
        item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
×
NEW
321
        item->setCheckState(defaults.contains(choice.id) ? Qt::Checked : Qt::Unchecked);
×
322
      }
NEW
323
      widg->setMaximumHeight(150);
×
NEW
324
      m_subForm->addWidget(widg);
×
NEW
325
      m_widgets[field.id]
×
NEW
326
          = {.label = lab, .widg = widg, .getValue = [widg, field]() -> ossia::value {
×
NEW
327
        QVariantList ids;
×
NEW
328
        for(int i = 0; i < widg->count(); i++)
×
NEW
329
          if(widg->item(i)->checkState() == Qt::Checked)
×
NEW
330
            ids.push_back(widg->item(i)->data(Qt::UserRole));
×
NEW
331
        return toSetting(field, ids);
×
NEW
332
      }, .setValue = [widg](ossia::value v) {
×
NEW
333
        QStringList ids;
×
NEW
334
        for(auto& e : ossia::convert<std::vector<ossia::value>>(v))
×
NEW
335
          ids.push_back(QString::fromStdString(ossia::convert<std::string>(e)));
×
NEW
336
        for(int i = 0; i < widg->count(); i++)
×
NEW
337
          widg->item(i)->setCheckState(
×
NEW
338
              ids.contains(widg->item(i)->data(Qt::UserRole).toString()) ? Qt::Checked
×
339
                                                                         : Qt::Unchecked);
NEW
340
      }};
×
NEW
341
    }
×
NEW
342
    else if(field.type == "colorpicker")
×
343
    {
NEW
344
      auto widg = new QLineEdit;
×
NEW
345
      widg->setText(colorToString(field.default_value));
×
NEW
346
      m_subForm->addWidget(widg);
×
NEW
347
      m_widgets[field.id]
×
NEW
348
          = {.label = lab, .widg = widg, .getValue = [widg, field]() -> ossia::value {
×
NEW
349
        if(field.default_json.isString() || field.returnType == "string")
×
NEW
350
          return widg->text().toStdString();
×
NEW
351
        return (int)(QColor(widg->text()).rgb() & 0xFFFFFF);
×
NEW
352
      }, .setValue = [widg](ossia::value v) {
×
NEW
353
        widg->setText(colorToString(v.apply(ossia::qt::ossia_to_qvariant{})));
×
354
      }};
×
355
    }
×
356
    else
357
    {
358
      // textinput, secret-text, bonjour-device, custom-variable
NEW
359
      auto widg = new QLineEdit;
×
NEW
360
      if(field.type.startsWith("secret"))
×
NEW
361
        widg->setEchoMode(QLineEdit::PasswordEchoOnEdit);
×
NEW
362
      if(!field.regex.isEmpty())
×
NEW
363
        makeValidator(widg, field.regex);
×
NEW
364
      widg->setText(field.default_value.toString());
×
NEW
365
      m_subForm->addWidget(widg);
×
NEW
366
      m_widgets[field.id]
×
NEW
367
          = {.label = lab, .widg = widg, .getValue = [widg, field]() -> ossia::value {
×
NEW
368
        return toSetting(field, widg->text());
×
NEW
369
      }, .setValue = [widg](ossia::value v) {
×
NEW
370
        widg->setText(QString::fromStdString(ossia::convert<std::string>(v)));
×
NEW
371
      }};
×
372
    }
NEW
373
    if(auto w = m_widgets.find(field.id); w != m_widgets.end() && w->second.widg)
×
NEW
374
      w->second.widg->setObjectName(field.id);
×
375
  }
376
  m_subForm->addStretch(1);
×
377
  m_fieldsLoaded = true;
×
378

NEW
379
  for(auto& field : m.config_fields)
×
NEW
380
    loadValue(
×
NEW
381
        field.id, field.hasDefault() ? std::optional{ossia::qt::qt_to_ossia{}(
×
NEW
382
                                           field.default_json.toVariant())}
×
NEW
383
                                     : std::nullopt);
×
NEW
384
  for(auto& [k, v] : m.config)
×
NEW
385
    loadValue(k, ossia::qt::qt_to_ossia{}(bitfocus::widenFloat(v)));
×
UNCOV
386
  for(auto& [k, v] : m_settings.configuration)
×
NEW
387
    loadValue(k, v);
×
NEW
388
}
×
389

NEW
390
void BitfocusProtocolSettingsWidget::loadValue(
×
391
    const QString& id, std::optional<ossia::value> v)
392
{
NEW
393
  auto member = m_widgets.find(id);
×
NEW
394
  if(member == m_widgets.end() || !member->second.setValue)
×
NEW
395
    return;
×
NEW
396
  auto& w = member->second;
×
NEW
397
  if(v)
×
NEW
398
    w.setValue(*v);
×
NEW
399
  w.shown = w.getValue();
×
NEW
400
  w.source = std::move(v);
×
UNCOV
401
}
×
402

403
void BitfocusProtocolSettingsWidget::resizeEvent(QResizeEvent* res)
×
404
{
405
  m_scroll->setMinimumWidth(0.8 * res->size().width());
×
406
  m_scroll->setMinimumHeight(0.8 * res->size().height());
×
407
}
×
408

409
void BitfocusProtocolSettingsWidget::setSettings(const Device::DeviceSettings& settings)
×
410
{
411
  if(m_hasInitLabel)
×
412
  {
413
    if(settings.deviceSpecificSettings.canConvert<BitfocusSpecificSettings>())
×
414
    {
415
      auto stgs = settings.deviceSpecificSettings.value<BitfocusSpecificSettings>();
×
416
      if(!stgs.path.isEmpty())
×
417
      {
418
        m_rootLayout->removeRow(1);
×
419
        m_hasInitLabel = false;
×
420
      }
×
421
    }
×
422
  }
×
423

424
  if(!settings.deviceSpecificSettings.canConvert<BitfocusSpecificSettings>())
×
425
  {
426
    resetFields();
×
427
    m_settings = {};
×
428
    m_deviceNameEdit->setText(settings.name);
×
429
    return;
×
430
  }
431

432
  auto stgs = settings.deviceSpecificSettings.value<BitfocusSpecificSettings>();
×
433
  stgs.deduplicateConfiguration();
×
434

435
  // Re-picking the module we already show would restart its process.
436
  if(m_settings.handler && !stgs.path.isEmpty() && stgs.path == m_settings.path
×
437
     && stgs.id == m_settings.id && stgs.product == m_settings.product)
×
438
    return;
×
439

440
  resetFields();
×
441
  m_deviceNameEdit->setText(settings.name);
×
442

443
  if(stgs.path.isEmpty() || !QDir{stgs.path}.exists())
×
444
  {
445
    m_settings = stgs;
×
446
    return;
×
447
  }
448

449
  // The enumerator lists modules by a label that is not a usable device name.
450
  if(!stgs.name.isEmpty() && settings.name == stgs.enumeratorLabel())
×
451
    m_deviceNameEdit->setText(stgs.name);
×
452

453
  // The handler is not serialized: start the module to get its config fields.
UNCOV
454
  if(!stgs.handler)
×
NEW
455
    stgs.handler = stgs.makeHandler(m_deviceNameEdit->text());
×
456

457
  m_settings = stgs;
×
458

459
  disconnect(m_configurationParsed);
×
460
  m_configurationParsed = connect(
×
461
      m_settings.handler.get(), &bitfocus::module_handler::configurationParsed, this,
×
462
      [this, h = std::weak_ptr{m_settings.handler}] {
×
463
    if(h.lock() == m_settings.handler)
×
464
      updateFields();
×
465
  });
×
466

NEW
467
  disconnect(m_configurationSaved);
×
NEW
468
  m_configurationSaved = connect(
×
NEW
469
      m_settings.handler.get(), &bitfocus::module_handler::configurationSaved, this,
×
NEW
470
      [this, h = std::weak_ptr{m_settings.handler}] {
×
NEW
471
    auto handler = h.lock();
×
NEW
472
    if(!handler || handler != m_settings.handler || !m_fieldsLoaded)
×
NEW
473
      return;
×
NEW
474
    for(auto& [k, v] : handler->model().config)
×
NEW
475
      if(auto w = m_widgets.find(k); w != m_widgets.end() && w->second.getValue
×
NEW
476
         && w->second.getValue() == w->second.shown)
×
NEW
477
        loadValue(k, ossia::qt::qt_to_ossia{}(bitfocus::widenFloat(v)));
×
NEW
478
  });
×
479

480
  // An already-running device answered long before we connected.
481
  if(!m_settings.handler->model().config_fields.empty())
×
482
    updateFields();
×
483
}
×
484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc