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

ossia / score / 36095816006

25 Sep 2026 04:46AM UTC coverage: 42.219% (-0.01%) from 42.23%
36095816006

Pull #2304

github

web-flow
Merge 509979a4d into c0c64ec39
Pull Request #2304: QML API additions, and three teardown fixes

39 of 229 new or added lines in 13 files covered. (17.03%)

160 existing lines in 12 files now uncovered.

125818 of 298010 relevant lines covered (42.22%)

117820.05 hits per line

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

54.59
/src/plugins/score-plugin-protocols/Protocols/Mapper/MapperDevice.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

4
#include <ossia-qt/qml_protocols.hpp>
5
#if __has_include(<QQmlEngine>)
6
#include <Process/Script/ScriptWidget.hpp>
7

8
#include <Explorer/DeviceList.hpp>
9
#include <Explorer/DeviceLogging.hpp>
10
#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
11

12
#include <Protocols/LibraryDeviceEnumerator.hpp>
13
#include <Protocols/Mapper/MapperDevice.hpp>
14

15
#include <score/serialization/AnySerialization.hpp>
16
#include <score/serialization/MapSerialization.hpp>
17
#include <score/tools/Bind.hpp>
18
#include <score/widgets/Layout.hpp>
19
#include <score/widgets/TextLabel.hpp>
20

21
#include <ossia/detail/config.hpp>
22

23
#include <ossia/detail/hash_map.hpp>
24
#include <ossia/detail/logger.hpp>
25
#include <ossia/network/base/protocol.hpp>
26
#include <ossia/network/common/node_visitor.hpp>
27
#include <ossia/network/generic/wrapped_parameter.hpp>
28

29
#include <ossia-qt/invoke.hpp>
30
#include <ossia-qt/js_utilities.hpp>
31
#include <ossia-qt/qml_engine_functions.hpp>
32

33
#include <QCodeEditor>
34
#include <QLineEdit>
35
#include <QObject>
36
#include <QPlainTextEdit>
37
#include <QPushButton>
38
#include <QQmlComponent>
39
#include <QQmlContext>
40
#include <QQmlEngine>
41
#include <QSplitter>
42
#include <QThread>
43
#include <QTimer>
44
#include <QTimerEvent>
45
#include <QUrl>
46

47
#include <wobjectimpl.h>
48

49
#include <verdigris>
50
namespace ossia::net
51
{
52
class observable_device_roots final : public QObject
53
{
54
  W_OBJECT(observable_device_roots)
55
public:
56
  explicit observable_device_roots(Device::DeviceList& devices)
132 ✔
57
  {
66 ✔
58
    devices.apply([this](auto& dev) { on_deviceAdded(&dev); });
200 ✔
59
    con(devices, &Device::DeviceList::deviceAdded, this,
132 ✔
60
        &observable_device_roots::on_deviceAdded);
66 ✔
61

62
    con(devices, &Device::DeviceList::deviceRemoved, this,
132 ✔
63
        &observable_device_roots::on_deviceRemoved);
66 ✔
64
  }
66 ✔
65
  ~observable_device_roots()
66 ✔
66
  {
×
67
  }
66 ✔
68

69
  void on_deviceAdded(const Device::DeviceInterface* d)
202 ✔
70
  {
71
    const int n = ++m_updating_index;
202 ✔
72
    connect(
202 ✔
73
        d, &Device::DeviceInterface::deviceChanged, this,
202 ✔
74
        &observable_device_roots::on_deviceAddedCallback, Qt::UniqueConnection);
75
    if(auto dev = d->getDevice())
202 ✔
76
    {
77
      m_devices.push_back(dev);
202 ✔
78
      notify_added(dev);
202 ✔
79
    }
202 ✔
80

81
    QTimer::singleShot(1, this, [this, n] {
404 ✔
82
      rootsChanged(roots(), n);
202 ✔
83
    });
202 ✔
84
  }
202 ✔
85

86
  void
87
  on_deviceAddedCallback(ossia::net::device_base* oldd, ossia::net::device_base* newd)
44 ✔
88
  {
89
    const int n = ++m_updating_index;
44 ✔
90
    notify_removing(oldd);
44 ✔
91
    ossia::remove_erase(m_devices, oldd);
44 ✔
92
    if(newd)
44 ✔
93
    {
94
      m_devices.push_back(newd);
×
95
      notify_added(newd);
×
96
    }
×
97

98
    QTimer::singleShot(1, this, [this, n] {
44 ✔
99
      rootsChanged(roots(), n);
×
100
    });
×
101
  }
44 ✔
102

103
  void on_deviceRemoved(const Device::DeviceInterface* d)
24 ✔
104
  {
105
    const int n = ++m_updating_index;
24 ✔
106
    disconnect(
24 ✔
107
        d, &Device::DeviceInterface::deviceChanged, this,
24 ✔
108
        &observable_device_roots::on_deviceAddedCallback);
109
    notify_removing(d->getDevice());
24 ✔
110
    ossia::remove_erase(m_devices, d->getDevice());
24 ✔
111

112
    QTimer::singleShot(1, this, [this, n] { rootsChanged(roots(), n); });
24 ✔
113
  }
24 ✔
114

115
  //! Removal must reach the cache while the device is still alive, so that no
116
  //! resolved parameter pointer outlives it: rootsChanged is queued to the
117
  //! mapper thread and runs too late. Registration comes from the mapper
118
  //! thread while the notify hooks run on the main one, hence the atomic.
119
  void set_engine_functions(ossia::qt::qml_engine_functions* f) noexcept
132 ✔
120
  {
121
    m_functions = f;
132 ✔
122
  }
132 ✔
123

124
  void notify_added(ossia::net::device_base* d)
202 ✔
125
  {
126
    if(auto* f = m_functions.load())
202 ✔
127
      f->addDevice(d);
×
128
  }
202 ✔
129

130
  void notify_removing(ossia::net::device_base* d)
68 ✔
131
  {
132
    if(!d)
68 ✔
133
      return;
×
134
    if(auto* f = m_functions.load())
68 ✔
135
      f->removeDevice(d);
68 ✔
136
  }
68 ✔
137

138
  void rootsChanged(std::vector<ossia::net::node_base*> a, int64_t i)
202 ✔
139
      W_SIGNAL(rootsChanged, a, i);
202 ✔
140

141
  std::vector<ossia::net::node_base*> roots() const noexcept
268 ✔
142
  {
143
    std::vector<ossia::net::node_base*> r;
268 ✔
144
    r.reserve(m_devices.size());
268 ✔
145
    for(auto d : m_devices) {
1,024 ✔
146
      r.push_back(&d->get_root_node());
756 ✔
147
    }
148
    return r;
268 ✔
149
  }
268 ✔
150
  const auto& devices() const noexcept { return m_devices; }
66 ✔
151

152
  std::atomic_int64_t m_updating_index = 0;
66 ✔
153

154
private:
155
  std::atomic<ossia::qt::qml_engine_functions*> m_functions{};
66 ✔
156
  std::vector<ossia::net::device_base*> m_devices;
157
};
158

159
ossia::net::parameter_base* find_parameter(
×
160
    ossia::net::node_base& root, const std::vector<ossia::net::node_base*>& roots,
161
    const QString& str)
162
{
163
  auto d = str.indexOf(':');
×
164
  if(d == -1)
×
165
  {
166
    // Address looks like '/foo/bar'
167
    // Try to find automatically in current root
168
    if(auto node = find_node(root, str.toStdString()))
×
169
    {
170
      return node->get_parameter();
×
171
    }
172
  }
×
173

174
  // Split in devices
175
  auto dev
176
      = ossia::find_if(roots, [devname = str.mid(0, d).toStdString()](const auto& dev) {
×
177
          return dev && dev->get_name() == devname;
×
178
        });
179

180
  if(dev != roots.end())
×
181
  {
182
    if(d == str.size() - 1)
×
183
    {
184
      return (*dev)->get_parameter();
×
185
    }
186

187
    if(auto node = find_node(**dev, str.mid(d + 1).toStdString()))
×
188
    {
189
      return node->get_parameter();
×
190
    }
191
  }
×
192

193
  // TODO handle path traversals... foo:/bar/*, etc
194
  return nullptr;
×
195
}
×
196

197
static ossia::small_vector<ossia::net::parameter_base*, 4> setup_sources(
×
198
    const QJSValue& jsval, ossia::net::node_base& self,
199
    const std::vector<ossia::net::node_base*>& roots)
200
{
201
  ossia::small_vector<ossia::net::parameter_base*, 4> res;
×
202
  if(jsval.isString())
×
203
  {
204
    res.push_back(find_parameter(self, roots, jsval.toString()));
×
205
  }
×
206
  else if(jsval.isArray())
×
207
  {
208
    QJSValueIterator it(jsval);
×
209
    while(it.hasNext())
×
210
    {
211
      it.next();
×
212
      if(const auto& val = it.value(); val.isString())
×
213
      {
214
        res.push_back(find_parameter(self, roots, val.toString()));
×
215
      }
×
216
      else
217
      {
218
        res.push_back(nullptr);
×
219
      }
220
    }
221
  }
×
222
  return res;
×
223
}
×
224

225
static void apply_reply(
×
226
    ossia::net::node_base& self, const std::vector<ossia::net::node_base*>& roots,
227
    const QJSValue& arr)
228
{
229
  // should be an array of { address, value } objects
230
  QJSValueIterator it(arr);
×
231
  while(it.hasNext())
×
232
  {
233
    it.next();
×
234
    auto val = it.value();
×
235
    auto addr = val.property("address");
×
236
    auto v = val.property("value");
×
237
    if(addr.isString() && !v.isNull())
×
238
    {
239
      auto addr_txt = addr.toString().toStdString();
×
240
      if(auto p = find_parameter(self, roots, addr.toString()))
×
241
        p->push_value(qt::value_from_js(p->value(), v));
×
242
    }
×
243
  }
×
244
}
×
245

246
static ossia::small_vector<ossia::value, 4> apply_reply(const QJSValue& arr)
×
247
{
248
  ossia::small_vector<ossia::value, 4> res;
×
249
  if(arr.isArray())
×
250
  {
251
    QJSValueIterator it(arr);
×
252
    while(it.hasNext())
×
253
    {
254
      it.next();
×
255
      res.push_back(ossia::qt::value_from_js(it.value()));
×
256
    }
257
  }
×
258
  return res;
×
259
}
×
260

261
struct mapper_parameter_data_base
262
{
263
  mapper_parameter_data_base() = default;
66 ✔
264
  mapper_parameter_data_base(const mapper_parameter_data_base&) = delete;
265
  mapper_parameter_data_base(mapper_parameter_data_base&& other)
1,050 ✔
266
      : bind{std::move(other.bind)}
525 ✔
267
      , read{std::move(other.read)}
525 ✔
268
      , write{std::move(other.write)}
525 ✔
269
      , interval{std::move(other.interval)}
525 ✔
270
      , source{std::move(other.source)}
525 ✔
271
  {
272
  }
525 ✔
273

274
  mapper_parameter_data_base& operator=(const mapper_parameter_data_base&) = delete;
275
  mapper_parameter_data_base& operator=(mapper_parameter_data_base&&) = delete;
276

277
  mapper_parameter_data_base(const QJSValue& val)
1,575 ✔
278
      : bind{val.property("bind")}
525 ✔
279
      , read{val.property("read")}
525 ✔
280
      , write{val.property("write")}
525 ✔
281
  {
282
    if(auto v = val.property("interval"); v.isNumber())
538 ✔
283
    {
284
      interval = v.toNumber();
13 ✔
285
    }
13 ✔
286
  }
525 ✔
287

288
  bool valid(const QJSValue& val) const noexcept
1,050 ✔
289
  {
290
    return !val.isUndefined() && !val.isNull();
1,050 ✔
291
  }
292
  bool valid() const noexcept
525 ✔
293
  {
294
    return true;
525 ✔
295
  }
296

297
  QJSValue bind;
298
  QJSValue read;
299
  QJSValue write;
300
  std::optional<double> interval;
301
  ossia::small_vector<ossia::net::parameter_base*, 4> source{};
591 ✔
302
  std::mutex source_lock;
303
};
304

305
struct mapper_parameter_data final
306
    : public parameter_data
307
    , public mapper_parameter_data_base
308
{
309
  using base_data_type = mapper_parameter_data_base;
310
  mapper_parameter_data() = default;
311
  mapper_parameter_data(const mapper_parameter_data&) = delete;
312
  mapper_parameter_data(mapper_parameter_data&&) = default;
313
  mapper_parameter_data& operator=(const mapper_parameter_data&) = delete;
314
  mapper_parameter_data& operator=(mapper_parameter_data&&) = delete;
315

316
  mapper_parameter_data(const std::string& name)
66 ✔
317
      : parameter_data{name}
66 ✔
318
  {
66 ✔
319
  }
66 ✔
320

321
  mapper_parameter_data(const QJSValue& val)
525 ✔
322
      : parameter_data{ossia::qt::make_parameter_data(val)}
525 ✔
323
      , mapper_parameter_data_base{val}
525 ✔
324
  {
525 ✔
325
  }
525 ✔
326
};
327

328
class mapper_protocol;
329
struct mapper_parameter final
330
    : wrapped_parameter<mapper_parameter_data>
331
    , Nano::Observer
332
{
333
public:
334
  using wrapped_parameter<mapper_parameter_data>::wrapped_parameter;
525 ✔
335
  ~mapper_parameter() override
525 ✔
336
  {
×
337
    for(auto& n : callbacks)
525 ✔
338
    {
339
      n.first->about_to_be_deleted.disconnect<&mapper_parameter::on_sourceRemoved>(
×
340
          *this);
341
      if(auto p = n.first->get_parameter())
×
342
        p->remove_callback(n.second);
×
343
    }
344

345
    callback_container<value_callback>::callbacks_clear();
525 ✔
346
  }
525 ✔
347

348
  void connect(ossia::net::parameter_base& s, mapper_protocol& proto);
349

350
  void on_sourceRemoved(const ossia::net::node_base& s)
×
351
  {
352
    std::lock_guard g{data().source_lock};
×
353
    if(auto p = s.get_parameter())
×
354
      ossia::remove_erase(data().source, p);
×
355

356
    callbacks.erase(&s);
×
357
  }
×
358

359
  struct callback_stopper
360
  {
361
    mapper_parameter& self;
362
    callback_stopper(mapper_parameter& self)
634 ✔
363
        : self{self}
634 ✔
364
    {
365
      self.m_stop_callbacks = true;
634 ✔
366
    }
634 ✔
367
    ~callback_stopper() { self.m_stop_callbacks = false; }
634 ✔
368
  };
369

370
  callback_stopper stop_callbacks() { return *this; }
634 ✔
371
  std::atomic_bool m_stop_callbacks = false;
525 ✔
372
  ossia::hash_map<const ossia::net::node_base*, ossia::net::parameter_base::iterator>
373
      callbacks;
374
};
375
using mapper_node = ossia::net::wrapped_node<mapper_parameter_data, mapper_parameter>;
376

377
class mapper_protocol final
378
    : public QObject
379
    , public ossia::net::protocol_base
380
{
381
  W_OBJECT(mapper_protocol)
382
public:
383
  mapper_protocol(
198 ✔
384
      const QByteArray& code, ossia::net::network_context_ptr ctx,
385
      Device::DeviceList& roots)
386
      : protocol_base{flags{}}
66 ✔
387
      , m_thread{std::make_shared<QThread>()}
66 ✔
388
      , m_context{ctx}
66 ✔
389
      , m_code{code}
66 ✔
390
      , m_devices{roots}
66 ✔
391
      , m_roots{m_devices.roots()}
66 ✔
392
  {
66 ✔
393
    this->moveToThread(m_thread.get());
66 ✔
394
    m_thread->start();
66 ✔
395
    m_hasInit++;
66 ✔
396
    QMetaObject::invokeMethod(this, &mapper_protocol::init_engine, Qt::QueuedConnection);
66 ✔
397
  }
66 ✔
398

399
  ~mapper_protocol() override
132 ✔
400
  {
66 ✔
401
    if(m_engine)
66 ✔
402
    {
403
      stop();
×
404
    }
×
405
  }
132 ✔
406

407
  void stop() override
66 ✔
408
  {
409
    // Necessary for the case where we're quickly redoing a full undo stack
410
    // which creates and then deletes the Mapper.
411
    // - otherwise there's a race between this and init_engine - we have to wait
412
    // for init_engine to complete so that we can delete everything safely, as we cannot
413
    // delete m_engine on another thread than our m_thread.
414
    while(m_hasInit > 0)
66 ✔
415
      std::this_thread::yield();
×
416

417
    // Both this and the removal hooks run on the main thread, so unregistering
418
    // here guarantees no hook can reach the engine functions afterwards.
419
    // disable() waits for any script currently inside Device.read/write and
420
    // no-ops later calls: the device tree below us is about to be destroyed.
421
    m_devices.set_engine_functions(nullptr);
66 ✔
422
    if(auto* fun = m_deviceFunctions.exchange(nullptr))
66 ✔
423
      fun->disable();
66 ✔
424

425
    auto engine = m_engine.load();
66 ✔
426
    auto comp = m_component.load();
66 ✔
427
    m_engine = nullptr;
66 ✔
428
    m_component = nullptr;
66 ✔
429
    SCORE_ASSERT(m_thread->isRunning());
66 ✔
430

431
    QMetaObject::invokeMethod(this, [this, comp, engine, t = QThread::currentThread()] {
132 ✔
432
      delete comp;
66 ✔
433
      delete engine;
66 ✔
434
      if(t)
66 ✔
435
        this->moveToThread(t);
66 ✔
436

437
      m_thread->exit();
66 ✔
438
    }, Qt::QueuedConnection);
66 ✔
439

440
    m_thread->wait();
66 ✔
441
  }
66 ✔
442

443
  void teardown_engine(QThread* t) { }
444

445
  void init_engine()
66 ✔
446
  {
447
    m_engine = new QQmlEngine{};
66 ✔
448
    m_component = new QQmlComponent{m_engine};
66 ✔
449

450
    auto device_obj = new ossia::qt::qml_device_engine_functions{
132 ✔
451
        {}, [](ossia::net::parameter_base& param, const ossia::value_port& v) {
402 ✔
452
      if(v.get_data().empty())
336 ✔
453
        return;
×
454
      auto& last = v.get_data().back().value;
336 ✔
455
      param.push_value(last);
336 ✔
456
    }, *m_engine, m_engine};
402 ✔
457
    device_obj->setDevice(m_device);
66 ✔
458
    for(auto dev : m_devices.devices())
268 ✔
459
      device_obj->addDevice(dev);
202 ✔
460
    m_deviceFunctions = device_obj;
66 ✔
461
    m_devices.set_engine_functions(device_obj);
66 ✔
462

463
    auto protocols_obj = new ossia::qt::qml_protocols{this->m_context, this};
66 ✔
464

465
    auto ctx = m_engine.load()->rootContext();
66 ✔
466
    ctx->setContextProperty("Device", device_obj);
66 ✔
467
    ctx->setContextProperty("Protocols", protocols_obj);
66 ✔
468

469
    QObject::connect(
66 ✔
470
        this, &mapper_protocol::sig_push, this, &mapper_protocol::slot_push);
471
    QObject::connect(
66 ✔
472
        this, &mapper_protocol::sig_recv, this, &mapper_protocol::slot_recv);
473
    con(m_devices, &observable_device_roots::rootsChanged, this,
132 ✔
474
        [this](std::vector<ossia::net::node_base*> r, int64_t n) {
66 ✔
UNCOV
475
      if(m_devices.m_updating_index != n)
×
UNCOV
476
        return;
×
477

478
      // The device list is not rebuilt from `r`: those raw node pointers
479
      // crossed a queued connection, so reaching their device from here would
480
      // be a use-after-free. notify_added/notify_removing maintain it
481
      // synchronously instead.
UNCOV
482
      m_roots = std::move(r);
×
UNCOV
483
      reset_tree();
×
UNCOV
484
    },
×
485
        Qt::QueuedConnection);
66 ✔
486

487
    QObject::connect(
66 ✔
488
        m_component, &QQmlComponent::statusChanged, this,
66 ✔
489
        [this](QQmlComponent::Status status) {
132 ✔
490
      if(!m_device)
66 ✔
491
        return;
×
492

493
      // Queued slots and lambdas can still run on the mapper thread after
494
      // stop() released the engine and the component from the main thread.
495
      auto engine = m_engine.load();
66 ✔
496
      auto comp = m_component.load();
66 ✔
497
      if(!engine || !comp)
66 ✔
498
        return;
×
499

500
      switch(status)
66 ✔
501
      {
502
        case QQmlComponent::Status::Ready: {
503
          if((m_object = comp->create()))
66 ✔
504
          {
505
            m_object->setParent(engine->rootContext());
66 ✔
506

507
            QVariant ret;
66 ✔
508
            QMetaObject::invokeMethod(
66 ✔
509
                m_object, "createTree", Q_RETURN_ARG(QVariant, ret));
66 ✔
510
            qt::create_device<ossia::net::device_base, mapper_node, mapper_protocol>(
66 ✔
511
                *m_device, ret.value<QJSValue>());
66 ✔
512
            reset_tree();
66 ✔
513
          }
66 ✔
514
          else
515
          {
516
            qDebug() << "Mapper: could not create object";
×
517
          }
518
          return;
66 ✔
519
        }
520
        case QQmlComponent::Status::Loading:
521
          return;
×
522
        case QQmlComponent::Status::Null:
523
        case QQmlComponent::Status::Error:
524
          qDebug() << comp->errorString();
×
525
          return;
×
526
      }
527
    });
66 ✔
528

529
    m_hasInit--;
66 ✔
530
  }
66 ✔
531

532
  void sig_push(mapper_parameter* p, const ossia::value& v) W_SIGNAL(sig_push, p, v);
634 ✔
533
  void
534
  sig_recv(mapper_parameter* p, ossia::net::parameter_base* s, const ossia::value& v)
×
535
      W_SIGNAL(sig_recv, p, s, v);
×
536

537
  static bool isAddressValueArray(const QJSValue& v)
×
538
  {
539
    if(!v.isArray())
×
540
      return false;
×
541
    const int len = v.property("length").toInt();
×
542
    if(len == 0)
×
543
      return true;
×
544
    for(int i = 0; i < len; i++)
×
545
    {
546
      if(const auto& obj = v.property(i); obj.isObject())
×
547
      {
548
        if(obj.hasProperty("address") && obj.hasProperty("value"))
×
549
          continue;
×
550
      }
×
551
      else
552
      {
553
        return false;
×
554
      }
555
    }
×
556
    return true;
×
557
  }
×
558
  void slot_push(mapper_parameter* param, const ossia::value& v)
634 ✔
559
  {
560
    auto engine = m_engine.load();
634 ✔
561
    if(!engine)
634 ✔
562
      return;
×
563

564
    auto& addr = *param;
634 ✔
565
    auto& dat = addr.data();
634 ✔
566
    auto cb = param->stop_callbacks();
634 ✔
567

568
    bool write = dat.write.isCallable();
634 ✔
569
    bool bound = dat.bind.isString() || dat.bind.isArray();
634 ✔
570
    if(!write && bound)
634 ✔
571
    {
572
      std::lock_guard g{dat.source_lock};
×
573
      for(auto p : dat.source)
×
574
      {
575
        if(p)
×
576
        {
577
          p->push_value(v);
×
578
        }
×
579
      }
580
    }
×
581
    else if(write)
634 ✔
582
    {
583
      auto res = dat.write.call({qt::value_to_js_value(v, *engine)});
272 ✔
584
      if(bound)
272 ✔
585
      {
586
        if(res.isArray())
×
587
        {
588
          if(isAddressValueArray(res))
×
589
          {
590
            std::lock_guard l{m_rootLock};
×
591
            apply_reply(m_device->get_root_node(), m_roots, res);
×
592
          }
×
593
          else
594
          {
595
            const auto r = apply_reply(res);
×
596
            auto cb = param->stop_callbacks();
×
597
            std::lock_guard g{dat.source_lock};
×
598
            auto N = std::min(r.size(), dat.source.size());
×
599
            for(std::size_t i = 0; i < N; i++)
×
600
            {
601
              if(r[i].valid() && dat.source[i])
×
602
              {
603
                dat.source[i]->push_value(r[i]);
×
604
              }
×
605
            }
×
606
          }
×
607
        }
×
608
        else
609
        {
610
          const auto val = ossia::qt::value_from_js(res);
×
611
          std::lock_guard g{dat.source_lock};
×
612
          for(auto p : dat.source)
×
613
          {
614
            if(p)
×
615
            {
616
              p->push_value(val);
×
617
            }
×
618
          }
619
        }
×
620
      }
×
621
      else
622
      {
623
        if(res.isArray())
272 ✔
624
        {
625
          std::lock_guard l{m_rootLock};
×
626
          apply_reply(m_device->get_root_node(), m_roots, res);
×
627
        }
×
628
      }
629
    }
272 ✔
630
  }
634 ✔
631

632
  void
633
  slot_recv(mapper_parameter* p, ossia::net::parameter_base* s, const ossia::value& v)
×
634
  {
635
    auto engine = m_engine.load();
×
636
    if(!engine)
×
637
      return;
×
638

639
    if(!p->data().read.isCallable())
×
640
    {
641
      p->push_value(v);
×
642
    }
×
643
    else
644
    {
645
      auto res = p->data().read.call(
×
646
          {QString::fromStdString(s->get_node().osc_address()),
×
647
           qt::value_to_js_value(v, *engine)});
×
648

649
      if(res.isArray())
×
650
      {
651
        if(res.property(0).isObject())
×
652
        {
653
          std::lock_guard l{m_rootLock};
×
654
          apply_reply(m_device->get_root_node(), m_roots, res);
×
655
        }
×
656
        else
657
        {
658
          p->push_value(qt::value_from_js(std::move(res)));
×
659
        }
660
      }
×
661
      else
662
      {
663
        p->push_value(qt::value_from_js(std::move(res)));
×
664
      }
665
    }
×
666
  }
×
667

668
  static mapper_parameter_data read_data(const QJSValue& js) { return js; }
525 ✔
669

670
private:
671
  void reset_tree()
66 ✔
672
  {
673
    // Initialize the roots
674
    if(!m_device)
66 ✔
675
      return;
×
676

677
    {
678
      std::lock_guard g{m_timersLock};
66 ✔
679
      for(auto [timer, ptr] : m_timers)
66 ✔
680
      {
UNCOV
681
        killTimer(timer);
×
682
      }
683
      m_timers.clear();
66 ✔
684
    }
66 ✔
685

686
    std::lock_guard l{m_rootLock};
66 ✔
687

688
    ossia::net::visit_parameters(
66 ✔
689
        m_device->get_root_node(), [&](auto& root, auto& param) {
591 ✔
690
          mapper_parameter& p = (mapper_parameter&)param;
525 ✔
691
          mapper_parameter_data_base& data = p.data();
525 ✔
692

693
          if(data.valid(data.bind))
525 ✔
694
          {
695
            std::lock_guard g{data.source_lock};
×
696
            data.source = setup_sources(data.bind, m_device->get_root_node(), m_roots);
×
697

698
            for(auto s : data.source)
×
699
            {
700
              if(s)
×
701
              {
702
                p.connect(*s, *this);
×
703
              }
×
704
            }
705
          }
×
706
          else if(data.valid(data.read) && data.interval)
525 ✔
707
          {
708
            double msecs = *data.interval;
13 ✔
709
            int timer_id = startTimer(msecs, Qt::PreciseTimer);
13 ✔
710

711
            std::lock_guard g{m_timersLock};
13 ✔
712
            m_timers[timer_id] = &p;
13 ✔
713
          }
13 ✔
714
        });
525 ✔
715
  }
66 ✔
716

717
  void timerEvent(QTimerEvent* ev) override
11 ✔
718
  {
719
    // Same window: the polling timers keep firing until the event loop exits.
720
    if(!m_engine.load())
11 ✔
721
      return;
×
722

723
    if(auto it = m_timers.find(ev->timerId()); it != m_timers.end())
11 ✔
724
    {
725
      if(auto p = it->second; p && p->data().read.isCallable())
11 ✔
726
      {
727
        auto v = qt::value_from_js(p->data().read.call({}));
11 ✔
728
        if(v != p->value())
11 ✔
729
          p->set_value(v);
9 ✔
730
      }
11 ✔
731
    }
11 ✔
732
  }
11 ✔
733

734
  bool pull(ossia::net::parameter_base&) override
×
735
  {
736
    // TODO
737
    return false;
×
738
  }
739

740
  bool
741
  push(const ossia::net::parameter_base& parameter_base, const ossia::value& v) override
634 ✔
742
  {
743
    sig_push((mapper_parameter*)&parameter_base, v);
634 ✔
744
    return true;
634 ✔
745
  }
746

747
  bool push_raw(const full_parameter_data& parameter_base) override { return false; }
×
748

749
  bool observe(ossia::net::parameter_base&, bool b) override { return false; }
×
750

751
  bool update(ossia::net::node_base& node_base) override { return true; }
66 ✔
752

753
  void set_device(device_base& dev) override
66 ✔
754
  {
755
    m_device = &dev;
66 ✔
756
    ossia::qt::run_async(this, [this, &dev] {
132 ✔
757
      // Runs after init_engine(), which the constructor posted first: the
758
      // script-facing object knows its owning device before the script loads,
759
      // so an unqualified address resolves here rather than in a sibling
760
      // device exposing the same leaf name.
761
      if(auto* fun = m_deviceFunctions.load())
66 ✔
762
        fun->setDevice(&dev);
66 ✔
763
      if(m_component)
66 ✔
764
        m_component.load()->setData(m_code, QUrl{});
66 ✔
765
    });
66 ✔
766
  }
66 ✔
767

768
private:
769
  std::shared_ptr<QThread> m_thread;
770
  std::atomic_int m_hasInit = 0;
66 ✔
771
  std::atomic<QQmlEngine*> m_engine{};
66 ✔
772
  std::atomic<QQmlComponent*> m_component{};
66 ✔
773
  std::atomic<ossia::qt::qml_device_engine_functions*> m_deviceFunctions{};
66 ✔
774

775
  ossia::net::network_context_ptr m_context{};
776
  ossia::net::device_base* m_device{};
66 ✔
777
  QObject* m_object{};
66 ✔
778
  QByteArray m_code;
779

780
  observable_device_roots m_devices;
781

782
  std::mutex m_rootLock;
783
  std::vector<ossia::net::node_base*> m_roots;
784

785
  std::mutex m_timersLock;
786
  ossia::hash_map<int, mapper_parameter*> m_timers;
787
};
788

789
using mapper_device = ossia::net::wrapped_device<mapper_node, mapper_protocol>;
790

791
void mapper_parameter::connect(parameter_base& s, mapper_protocol& proto)
×
792
{
793
  set_value(s.value());
×
794
  s.get_node().about_to_be_deleted.connect<&mapper_parameter::on_sourceRemoved>(*this);
×
795
  // TODO handle parameter removal from device -> some hash_map
796
  auto it = callbacks.find(&s.get_node());
×
797
  if(it == callbacks.end())
×
798
  {
799
    QPointer<mapper_protocol> proto_ptr = &proto;
×
800
    callbacks[&s.get_node()]
×
801
        = s.add_callback([this, param = &s, proto_ptr](const ossia::value& v) {
×
802
            if(!this->m_stop_callbacks)
×
803
            {
804
              SCORE_ASSERT(proto_ptr);
×
805
              proto_ptr->sig_recv(this, param, v);
×
806
            }
×
807
          });
×
808
  }
×
809
  else
810
  {
811
    qDebug() << "Warning ! callback for" << s.get_node().osc_address().c_str()
×
812
             << "already exists";
×
813
    return;
×
814
  }
815
}
×
816

817
}
818

819
namespace Protocols
820
{
821

822
class MapperDevice final : public Device::OwningDeviceInterface
823
{
824
  W_OBJECT(MapperDevice)
825

826
public:
827
  MapperDevice(
66 ✔
828
      const Device::DeviceSettings& settings, const ossia::net::network_context_ptr& ctx,
829
      const score::DocumentContext& cctx)
830
      : OwningDeviceInterface{settings}
66 ✔
831
      , net_context{ctx}
66 ✔
832
      , context{cctx}
66 ✔
833
      , m_list{}
66 ✔
834
  {
66 ✔
835
    m_capas.canRefreshTree = true;
66 ✔
836
    m_capas.canAddNode = false;
66 ✔
837
    m_capas.canRemoveNode = false;
66 ✔
838
    m_capas.canSerialize = false;
66 ✔
839
    m_capas.canRenameNode = false;
66 ✔
840
    m_capas.canSetProperties = false;
66 ✔
841
  }
66 ✔
842

843
  bool reconnect() override
66 ✔
844
  {
845
    disconnect();
66 ✔
846

847
    auto devlist = devices();
66 ✔
848
    if(!devlist)
66 ✔
849
      return false;
×
850

851
    try
852
    {
853
      const auto& stgs
66 ✔
854
          = settings().deviceSpecificSettings.value<MapperSpecificSettings>();
66 ✔
855

856
      auto proto = std::make_unique<ossia::net::mapper_protocol>(
66 ✔
857
          stgs.text.toUtf8(), this->net_context, *devlist);
66 ✔
858
      auto nm = settings().name.toStdString();
66 ✔
859
      m_dev = std::make_unique<ossia::net::mapper_device>(
66 ✔
860
          static_cast<std::unique_ptr<ossia::net::mapper_protocol>&&>(proto), nm);
861

862
      deviceChanged(nullptr, m_dev.get());
66 ✔
863

864
      enableCallbacks();
66 ✔
865

866
      setLogging_impl(Device::get_cur_logging(isLogging()));
66 ✔
867
    }
66 ✔
868
    catch(std::exception& e)
869
    {
870
      qDebug() << "Could not connect: " << e.what();
×
871
    }
×
872
    catch(...)
873
    {
874
      // TODO save the reason of the non-connection.
875
    }
×
876

877
    return connected();
66 ✔
878
  }
66 ✔
879

880
  ~MapperDevice() override { }
132 ✔
881

882
private:
883
  Device::DeviceList* devices()
66 ✔
884
  {
885
    if(m_list)
66 ✔
886
      return m_list;
×
887

888
    auto plug = context.findPlugin<Explorer::DeviceDocumentPlugin>(); // list()
66 ✔
889
    if(plug)
66 ✔
890
      m_list = &plug->list();
66 ✔
891

892
    return m_list;
66 ✔
893
  }
66 ✔
894
  ossia::net::network_context_ptr net_context;
895
  const score::DocumentContext& context;
896
  Device::DeviceList* m_list{};
897
};
898

899
MapperProtocolFactory::~MapperProtocolFactory() { }
3,702 ✔
900
QString MapperProtocolFactory::prettyName() const noexcept
73 ✔
901
{
902
  return QObject::tr("Mapper");
73 ✔
903
}
904

905
QString MapperProtocolFactory::category() const noexcept
2 ✔
906
{
907
  return StandardCategories::util;
2 ✔
908
}
909

910
QUrl MapperProtocolFactory::manual() const noexcept
2 ✔
911
{
912
  return QUrl("https://ossia.io/score-docs/devices/mapper-device.html");
2 ✔
913
}
914
Device::DeviceEnumerators
915
MapperProtocolFactory::getEnumerators(const score::DocumentContext& ctx) const
1 ✔
916
{
917
  auto library_enumerator = new LibraryDeviceEnumerator{
2 ✔
918
      "Ossia.Mapper",
1 ✔
919
      {"qml"},
1 ✔
920
      MapperProtocolFactory::static_concreteKey(),
1 ✔
921
      [](const QByteArray& arr) {
1 ✔
922
    return QVariant::fromValue(MapperSpecificSettings{arr});
×
923
      },
×
924
      ctx};
1 ✔
925

926
  return {{"Library", library_enumerator}};
1 ✔
927
}
×
928

929
Device::DeviceInterface* MapperProtocolFactory::makeDevice(
66 ✔
930
    const Device::DeviceSettings& settings, const Explorer::DeviceDocumentPlugin& plugin,
931
    const score::DocumentContext& ctx)
932
{
933
  return new MapperDevice{settings, plugin.networkContext(), ctx};
66 ✔
934
}
×
935

936
const Device::DeviceSettings& MapperProtocolFactory::defaultSettings() const noexcept
1 ✔
937
{
938
  static const Device::DeviceSettings settings = [&]() {
2 ✔
939
    Device::DeviceSettings s;
1 ✔
940
    s.protocol = concreteKey();
1 ✔
941
    s.name = "Mapper";
1 ✔
942
    MapperSpecificSettings specif;
1 ✔
943
    s.deviceSpecificSettings = QVariant::fromValue(specif);
1 ✔
944
    return s;
1 ✔
945
  }();
1 ✔
946
  return settings;
1 ✔
947
}
948

949
Device::ProtocolSettingsWidget* MapperProtocolFactory::makeSettingsWidget()
×
950
{
951
  return new MapperProtocolSettingsWidget;
×
952
}
×
953

954
QVariant
955
MapperProtocolFactory::makeProtocolSpecificSettings(const VisitorVariant& visitor) const
70 ✔
956
{
957
  return makeProtocolSpecificSettings_T<MapperSpecificSettings>(visitor);
70 ✔
958
}
959

960
void MapperProtocolFactory::serializeProtocolSpecificSettings(
94 ✔
961
    const QVariant& data, const VisitorVariant& visitor) const
962
{
963
  serializeProtocolSpecificSettings_T<MapperSpecificSettings>(data, visitor);
94 ✔
964
}
94 ✔
965

966
bool MapperProtocolFactory::checkCompatibility(
×
967
    const Device::DeviceSettings& a, const Device::DeviceSettings& b) const noexcept
968
{
969
  return true;
×
970
}
971

972
MapperProtocolSettingsWidget::MapperProtocolSettingsWidget(QWidget* parent)
×
973
    : ProtocolSettingsWidget(parent)
×
974
{
×
975
  QLabel* deviceNameLabel = new TextLabel(tr("Name"), this);
×
976
  m_name = new QLineEdit;
×
977

978
  m_codeEdit = Process::createScriptWidget("JS");
×
979

980
  m_errorPane = new QPlainTextEdit{this};
×
981
  m_errorPane->setReadOnly(true);
×
982
  m_errorPane->setMaximumHeight(120);
×
983

984
  m_splitter = new QSplitter{Qt::Vertical, this};
×
985
  m_splitter->addWidget(m_codeEdit);
×
986
  m_splitter->addWidget(m_errorPane);
×
987
  m_splitter->setStretchFactor(0, 3);
×
988
  m_splitter->setStretchFactor(1, 1);
×
989

990
  auto validateBtn = new QPushButton{tr("Validate"), this};
×
991
  connect(validateBtn, &QPushButton::clicked, this, &MapperProtocolSettingsWidget::validate);
×
992

993
  QGridLayout* gLayout = new QGridLayout;
×
994

995
  gLayout->addWidget(deviceNameLabel, 0, 0, 1, 1);
×
996
  gLayout->addWidget(m_name, 0, 1, 1, 1);
×
997
  gLayout->addWidget(m_splitter, 3, 0, 1, 2);
×
998
  gLayout->addWidget(validateBtn, 4, 0, 1, 2);
×
999

1000
  setLayout(gLayout);
×
1001

1002
  setDefaults();
×
1003
}
×
1004

1005
void MapperProtocolSettingsWidget::setDefaults()
×
1006
{
1007
  SCORE_ASSERT(m_codeEdit);
×
1008

1009
  m_name->setText("newDevice");
×
1010
  m_codeEdit->setPlainText("");
×
1011
}
×
1012

1013
void MapperProtocolSettingsWidget::validate()
×
1014
{
1015
  m_errorPane->clear();
×
1016

1017
  auto code = m_codeEdit->toPlainText().toUtf8();
×
1018
  if(code.isEmpty())
×
1019
    return;
×
1020

1021
  auto engine = new QQmlEngine{this};
×
1022
  auto comp = new QQmlComponent{engine};
×
1023

1024
  connect(
×
1025
      comp, &QQmlComponent::statusChanged, this,
×
1026
      [this, comp, engine](QQmlComponent::Status status) {
×
1027
    switch(status)
×
1028
    {
1029
      case QQmlComponent::Status::Ready:
1030
        m_errorPane->setPlainText(tr("QML code is valid."));
×
1031
        break;
×
1032
      case QQmlComponent::Status::Error: {
1033
        auto errors = comp->errorString();
×
1034
        if(errors.startsWith(':'))
×
1035
          errors = QStringLiteral("L") + errors.mid(1);
×
1036
        errors.replace(QStringLiteral("\n:"), QStringLiteral("\nL"));
×
1037
        qDebug() << "Mapper:" << errors;
×
1038
        m_errorPane->setPlainText(errors);
×
1039
      } break;
×
1040
      default:
1041
        break;
×
1042
    }
1043
    comp->deleteLater();
×
1044
    engine->deleteLater();
×
1045
  });
×
1046

1047
  comp->setData(code, QUrl{});
×
1048
}
×
1049

1050
Device::DeviceSettings MapperProtocolSettingsWidget::getSettings() const
×
1051
{
1052
  Device::DeviceSettings s;
×
1053
  s.name = m_name->text();
×
1054
  s.protocol = MapperProtocolFactory::static_concreteKey();
×
1055

1056
  s.deviceSpecificSettings
×
1057
      = QVariant::fromValue(MapperSpecificSettings{m_codeEdit->toPlainText()});
×
1058
  return s;
×
1059
}
×
1060

1061
void MapperProtocolSettingsWidget::setSettings(const Device::DeviceSettings& settings)
×
1062
{
1063
  m_name->setText(settings.name);
×
1064
  MapperSpecificSettings specific;
×
1065
  if(settings.deviceSpecificSettings.canConvert<MapperSpecificSettings>())
×
1066
  {
1067
    specific = settings.deviceSpecificSettings.value<MapperSpecificSettings>();
×
1068

1069
    m_codeEdit->setPlainText(specific.text);
×
1070
  }
×
1071
}
×
1072
}
1073

1074
template <>
1075
void DataStreamReader::read(const Protocols::MapperSpecificSettings& n)
91 ✔
1076
{
1077
  m_stream << n.text;
91 ✔
1078
}
91 ✔
1079

1080
template <>
1081
void DataStreamWriter::write(Protocols::MapperSpecificSettings& n)
2 ✔
1082
{
1083
  m_stream >> n.text;
2 ✔
1084
}
2 ✔
1085

1086
template <>
1087
void JSONReader::read(const Protocols::MapperSpecificSettings& n)
3 ✔
1088
{
1089
  obj["Text"] = n.text;
3 ✔
1090
}
3 ✔
1091

1092
template <>
1093
void JSONWriter::write(Protocols::MapperSpecificSettings& n)
68 ✔
1094
{
1095
  n.text = obj["Text"].toString();
68 ✔
1096
}
68 ✔
1097

1098
Q_DECLARE_METATYPE(ossia::net::mapper_parameter*)
3 ✔
1099
W_REGISTER_ARGTYPE(ossia::net::mapper_parameter*)
1100
W_OBJECT_IMPL(Protocols::MapperDevice)
656 ✔
1101
W_OBJECT_IMPL(ossia::net::observable_device_roots)
66 ✔
1102
W_OBJECT_IMPL(ossia::net::mapper_protocol)
195 ✔
1103
#endif
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