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

ossia / score / 37965809739

09 Oct 2026 05:22PM UTC coverage: 48.076% (-0.5%) from 48.588%
37965809739

push

github

jcelerier
3rdparty: pin avendish to main, now that #219 is merged

celtera/avendish#219 merged as 797b3c9d7e, so the ColorDisplay fix this
branch needs is on main and the pin no longer has to name
fix/color-display-unit-aliases. Pinned to main's tip a1f6471c53, which
also picks up #220.

Content is unchanged where it matters: the tree at the old pin and at
797b3c9d7e is byte-identical (`git diff` reports no files), and #220 on
top touches one line -- a Catch2 tag inside `if(BUILD_TESTING)` in
cmake/avendish.tests.cmake -- which score never evaluates.

Note `git cherry` is useless for checking this: #219 was squash-merged, so
its 7 commits collapsed into 1 and no patch-id matches. Compare trees, not
commits. A pin on a feature branch dies when that branch is tidied up,
which is why this matters beyond neatness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QYyvhRXvSMSRQPaq12Teq

156491 of 325507 relevant lines covered (48.08%)

146731.39 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
#include <limits>
36

37
namespace Protocols
38
{
39

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

47
  m_rootLayout = new score::MarginLess<QFormLayout>{this};
×
48
  m_rootLayout->addRow(tr("Name"), m_deviceNameEdit);
×
49
  m_rootLayout->addRow(new QLabel{
×
50
      tr("Install \"Bitfocus Companion Modules\" in Settings > Package Manager.")});
×
51
  m_hasInitLabel = true;
×
52
  m_rootLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
×
53

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

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

72
  BitfocusSpecificSettings osc = m_settings;
×
73

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

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

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

111
  return s;
×
112
}
×
113

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

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

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

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

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

157
template <typename Spin>
158
static void setupEmptyNumber(Spin* widg, const bitfocus::module_data::config_field& field)
×
159
{
160
  if(field.hasDefault())
×
161
    return;
×
162
  using T = decltype(widg->minimum());
163
  constexpr T lowest = std::numeric_limits<T>::lowest();
×
164
  const T min = widg->minimum();
×
165
  const T step = widg->singleStep();
×
166
  widg->setMinimum(min >= lowest + step ? min - step : lowest);
×
167
  widg->setSpecialValueText(QStringLiteral(" "));
×
168
}
×
169

170
template <typename Spin>
171
static bool isEmptyNumber(Spin* widg, const bitfocus::module_data::config_field& field)
×
172
{
173
  return !field.hasDefault() && widg->value() == widg->minimum();
×
174
}
175

176
static QString colorToString(const QVariant& v)
×
177
{
178
  if(v.typeId() == QMetaType::QString)
×
179
    return v.toString();
×
180
  return QColor::fromRgb(QRgb(v.toLongLong() & 0xFFFFFF)).name();
×
181
}
×
182

183
void BitfocusProtocolSettingsWidget::updateFields()
×
184
{
185
  if(!m_settings.handler)
×
186
    return;
×
187

188
  resetFields();
×
189

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

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

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

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

384
  for(auto& field : m.config_fields)
×
385
    loadValue(
×
386
        field.id, field.hasDefault() ? std::optional{ossia::qt::qt_to_ossia{}(
×
387
                                           field.default_json.toVariant())}
×
388
                                     : std::nullopt);
×
389
  for(auto& [k, v] : m.config)
×
390
    loadValue(k, ossia::qt::qt_to_ossia{}(bitfocus::widenFloat(v)));
×
391
  for(auto& [k, v] : m_settings.configuration)
×
392
    loadValue(k, v);
×
393
}
×
394

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

408
void BitfocusProtocolSettingsWidget::resizeEvent(QResizeEvent* res)
×
409
{
410
  m_scroll->setMinimumWidth(0.8 * res->size().width());
×
411
  m_scroll->setMinimumHeight(0.8 * res->size().height());
×
412
}
×
413

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

429
  if(!settings.deviceSpecificSettings.canConvert<BitfocusSpecificSettings>())
×
430
  {
431
    resetFields();
×
432
    m_settings = {};
×
433
    m_deviceNameEdit->setText(settings.name);
×
434
    return;
×
435
  }
436

437
  auto stgs = settings.deviceSpecificSettings.value<BitfocusSpecificSettings>();
×
438
  stgs.deduplicateConfiguration();
×
439

440
  // Re-picking the module we already show would restart its process.
441
  if(m_settings.handler && !stgs.path.isEmpty() && stgs.path == m_settings.path
×
442
     && stgs.id == m_settings.id && stgs.product == m_settings.product)
×
443
    return;
×
444

445
  resetFields();
×
446
  m_deviceNameEdit->setText(settings.name);
×
447

448
  if(stgs.path.isEmpty() || !QDir{stgs.path}.exists())
×
449
  {
450
    m_settings = stgs;
×
451
    return;
×
452
  }
453

454
  // The enumerator lists modules by a label that is not a usable device name.
455
  if(!stgs.name.isEmpty() && settings.name == stgs.enumeratorLabel())
×
456
    m_deviceNameEdit->setText(stgs.name);
×
457

458
  // The handler is not serialized: start the module to get its config fields.
459
  if(!stgs.handler)
×
460
    stgs.handler = stgs.makeHandler(m_deviceNameEdit->text());
×
461

462
  m_settings = stgs;
×
463

464
  disconnect(m_configurationParsed);
×
465
  m_configurationParsed = connect(
×
466
      m_settings.handler.get(), &bitfocus::module_handler::configurationParsed, this,
×
467
      [this, h = std::weak_ptr{m_settings.handler}] {
×
468
    if(h.lock() == m_settings.handler)
×
469
      updateFields();
×
470
  });
×
471

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

485
  // An already-running device answered long before we connected.
486
  if(!m_settings.handler->model().config_fields.empty())
×
487
    updateFields();
×
488
}
×
489
}
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