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

ossia / score / 30213925900

26 Jul 2026 06:03PM UTC coverage: 15.298% (-0.01%) from 15.311%
30213925900

push

github

jcelerier
3rdparty: bump libossia for the network-context poll fix

Brings in ossia/libossia#913: SDL joystick init no longer requires haptic
support (which the Emscripten SDL2 port does not have), and
network_context::poll() restarts the io_context, without which the
WebAssembly polling path stops after its first tick.

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

30400 of 198713 relevant lines covered (15.3%)

997.67 hits per line

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

37.44
/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.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 "DeviceDocumentPlugin.hpp"
4

5
#include <State/Address.hpp>
6

7
#include <Device/Node/DeviceNode.hpp>
8
#include <Device/Protocol/DeviceInterface.hpp>
9
#include <Device/Protocol/DeviceSettings.hpp>
10
#include <Device/Protocol/ProtocolFactoryInterface.hpp>
11
#include <Device/Protocol/ProtocolList.hpp>
12

13
#include <Explorer/Commands/ReplaceDevice.hpp>
14
#include <Explorer/DeviceList.hpp>
15
#include <Explorer/DocumentPlugin/DeviceDocumentPluginFactory.hpp>
16
#include <Explorer/DocumentPlugin/NodeUpdateProxy.hpp>
17
#include <Explorer/Listening/ListeningHandlerFactoryList.hpp>
18

19
#include <score/application/ApplicationContext.hpp>
20
#include <score/application/GUIApplicationContext.hpp>
21
#include <score/command/Dispatchers/CommandDispatcher.hpp>
22
#include <score/document/DocumentContext.hpp>
23
#include <score/model/tree/TreeNode.hpp>
24
#include <score/plugins/InterfaceList.hpp>
25
#include <score/plugins/StringFactoryKey.hpp>
26
#include <score/plugins/documentdelegate/plugin/DocumentPlugin.hpp>
27
#include <score/serialization/VisitorCommon.hpp>
28
#include <score/tools/Bind.hpp>
29
#include <score/widgets/MessageBox.hpp>
30
#include <score/widgets/Pixmap.hpp>
31

32
#include <ossia/detail/logger.hpp>
33
#include <ossia/detail/thread.hpp>
34
#include <ossia/network/context.hpp>
35

36
#include <ossia-qt/invoke.hpp>
37

38
#include <QApplication>
39
#include <QDebug>
40
#include <QMainWindow>
41
#include <QMessageBox>
42
#include <QObject>
43
#include <QPushButton>
44
#include <QString>
45

46
#include <wobjectimpl.h>
47

48
#include <stdexcept>
49
#include <vector>
50
W_OBJECT_IMPL(Explorer::DeviceDocumentPlugin)
51✔
51
namespace Explorer
52
{
53
MODEL_METADATA_IMPL_CPP(DeviceDocumentPlugin)
75✔
54
DeviceDocumentPlugin::DeviceDocumentPlugin(
54✔
55
    const score::DocumentContext& ctx, QObject* parent)
56
    : score::SerializableDocumentPlugin{ctx, "Explorer::DeviceDocumentPlugin", parent}
18✔
57
{
36✔
58
  init();
18✔
59
  m_explorer = new DeviceExplorerModel{*this, this};
18✔
60
}
×
61

62
void DeviceDocumentPlugin::on_documentClosing()
20✔
63
{
64
  for(auto dev : this->m_list.devices())
20✔
65
  {
66
    dev->disconnect();
×
67
  }
68
}
20✔
69

70
DeviceDocumentPlugin::~DeviceDocumentPlugin()
40✔
71
{
20✔
72
  for(auto dev : this->m_list.devices())
20✔
73
  {
74
    dev->disconnect();
×
75
  }
76

77
  m_processMessages = false;
20✔
78
  m_asioContext->context.stop();
20✔
79

80
#if !defined(__EMSCRIPTEN__)
81
  m_asioThread.join();
20✔
82
#endif
83
}
40✔
84

85
// MOVEME
86
struct print_node_rec
87
{
88
  void visit(const Device::Node& addr)
89
  {
90
    qDebug() << Device::address(addr).toString();
91
    ;
92
    for(auto& child : addr)
93
    {
94
      visit(child);
95
    }
96
  }
97
};
98

99
void DeviceDocumentPlugin::init()
20✔
100
{
101
  m_asioContext = std::make_shared<ossia::net::network_context>();
20✔
102
  m_processMessages = true;
20✔
103
#if defined(__EMSCRIPTEN__)
104
  startTimer(8);
105
#else
106
  m_asioThread = std::thread{[this] {
40✔
107
    ossia::set_thread_name("ossia asio");
20✔
108
    while(m_processMessages)
40✔
109
    {
110
      m_asioContext->run();
20✔
111
    }
112
  }};
20✔
113
#endif
114
}
20✔
115

116
void DeviceDocumentPlugin::asyncConnect(Device::DeviceInterface& newdev)
×
117
{
118
  const auto w = score::GUIAppContext().mainWindow;
×
119
  if(newdev.capabilities().asyncConnect && w)
×
120
  {
121
    w->setEnabled(false);
×
122

123
    QMessageBox b(
×
124
        QMessageBox::NoIcon, QString{"Waiting"},
×
125
        QString{"Waiting for a device: " + newdev.settings().name},
×
126
        QMessageBox::StandardButton::NoButton, w,
×
127
        Qt::CustomizeWindowHint | Qt::WindowTitleHint);
×
128
    b.setStandardButtons(QMessageBox::StandardButton::Cancel);
×
129
    b.setIconPixmap(
×
130
        score::get_pixmap(QStringLiteral(":/icons/message_information.png")));
×
131

132
    connect(
×
133
        b.button(QMessageBox::StandardButton::Cancel), &QPushButton::clicked, &b,
×
134
        [&b] { b.reject(); });
×
135

136
    connect(
×
137
        &newdev, &Device::DeviceInterface::connectionChanged, &b, [&b] { b.accept(); });
×
138
    QTimer::singleShot(1, [&] { newdev.reconnect(); });
×
139
    b.exec();
×
140

141
    w->setEnabled(true);
×
142
  }
×
143
  else
144
  {
145
    newdev.reconnect();
×
146
  }
147
}
×
148

149
void DeviceDocumentPlugin::timerEvent(QTimerEvent* event)
×
150
{
151
#if defined(__EMSCRIPTEN__)
152
  if(m_processMessages)
153
    m_asioContext->poll();
154
#endif
155
}
×
156
/** The following code handles device creation / loading.
157
 *
158
 * There are multiple cases:
159
 * - Adding a new device:
160
 *   - Creating a device that won't have anything set-up (Default OSC device)
161
 *   - Creating a device with a pre-existing node (loading an OSC device through a device file)
162
 *   - Creating a device that needs refreshing (OSCQuery, Minuit, MIDI...)
163
 * - Loading a save file:
164
 *   - Same cases than above. When refreshing however the previous node is kept
165
 *     in case we want to work without e.g. a device available
166
 *   -> what happens for not found MIDI devices
167
 *   -> what happens for not found OSCQuery devices
168
 */
169

170
Device::Node DeviceDocumentPlugin::createDeviceFromNode(const Device::Node& node)
×
171
{
172
  try
173
  {
174
    auto& fact = m_context.app.interfaces<Device::ProtocolFactoryList>();
×
175

176
    // Instantiate a real device.
177
    auto proto = fact.get(node.get<Device::DeviceSettings>().protocol);
×
178
    auto newdev
×
179
        = proto->makeDevice(node.get<Device::DeviceSettings>(), *this, context());
×
180

181
    if(!newdev)
×
182
      throw std::runtime_error("Null device");
×
183

184
    initDevice(*newdev);
×
185

186
    if(newdev->capabilities().canRefreshTree)
×
187
    {
188
      return newdev->refresh();
×
189
    }
190
    else
191
    {
192
      for(auto& child : node)
×
193
      {
194
        newdev->addNode(child);
×
195
      }
196
      return node;
×
197
    }
198
  }
×
199
  catch(const std::runtime_error& e)
200
  {
201
    score::warning(
×
202
        QApplication::activeWindow(), QObject::tr("Error loading device"),
×
203
        node.get<Device::DeviceSettings>().name + ": " + QString::fromLatin1(e.what()));
×
204
  }
×
205

206
  return node;
×
207
}
×
208

209
std::optional<Device::Node>
210
DeviceDocumentPlugin::loadDeviceFromNode(const Device::Node& node)
×
211
{
212
  try
213
  {
214
    // Instantiate a real device.
215
    auto& fact = m_context.app.interfaces<Device::ProtocolFactoryList>();
×
216
    auto proto = fact.get(node.get<Device::DeviceSettings>().protocol);
×
217
    if(!proto)
×
218
      throw std::runtime_error("Null protocol");
×
219
    Device::DeviceInterface* newdev
×
220
        = proto->makeDevice(node.get<Device::DeviceSettings>(), *this, context());
×
221

222
    if(!newdev)
×
223
      throw std::runtime_error("Null device");
×
224

225
    initDevice(*newdev);
×
226

227
    // We do not reload for devices such as LocalDevice.
228
    if(newdev->capabilities().canSerialize)
×
229
    {
230
      for(auto& child : node)
×
231
      {
232
        newdev->addNode(child);
×
233
      }
234

235
      return {};
×
236
    }
237
    else
238
    {
239
      // In this case we instead explore the actual
240
      // device node.
241
      return newdev->refresh();
×
242
    }
243
  }
×
244
  catch(const std::runtime_error& e)
245
  {
246
    score::warning(
×
247
        QApplication::activeWindow(), QObject::tr("Error loading device"),
×
248
        node.get<Device::DeviceSettings>().name + ": " + QString::fromLatin1(e.what()));
×
249
  }
×
250

251
  return {};
×
252
}
×
253

254
void DeviceDocumentPlugin::setConnection(bool b)
40✔
255
{
256
  if(b)
40✔
257
  {
258
    // Reconnect all devices
259
    m_list.apply([&](Device::DeviceInterface& dev) {
60✔
260
      if(!dev.connected())
40✔
261
        asyncConnect(dev);
×
262
      if(dev.capabilities().canSerialize)
40✔
263
      {
264
        auto it = ossia::find_if(m_rootNode, [&](const Device::Node& dev_node) {
20✔
265
          return dev_node.template get<Device::DeviceSettings>().name
×
266
                 == dev.settings().name;
×
267
        });
268

269
        if(it != m_rootNode.cend())
20✔
270
        {
271
          for(const auto& nodes : *it)
×
272
          {
273
            dev.addNode(nodes);
×
274
          }
275
        }
×
276
        else
277
        {
278
          qDebug() << "Could not save device";
20✔
279
        }
280
      }
20✔
281

282
      setupConnections(dev, true);
40✔
283
    });
40✔
284
  }
20✔
285
  else
286
  {
287
    // Disconnect all devices
288
    m_list.apply([&](Device::DeviceInterface& dev) {
60✔
289
      setupConnections(dev, false);
40✔
290
      dev.disconnect();
40✔
291
    });
40✔
292
  }
293
}
40✔
294

295
ListeningHandler& DeviceDocumentPlugin::listening() const
31✔
296
{
297
  if(m_listening)
31✔
298
    return *m_listening;
12✔
299

300
  m_listening
38✔
301
      = context().app.interfaces<ListeningHandlerFactoryList>().make(*this, context());
38✔
302
  return *m_listening;
19✔
303
}
31✔
304

305
void DeviceDocumentPlugin::initDevice(Device::DeviceInterface& newdev)
×
306
{
307
  asyncConnect(newdev);
×
308
  newdev.valueUpdated.connect<&DeviceDocumentPlugin::on_valueUpdated>(*this);
×
309

310
  setupConnections(newdev, true);
×
311

312
  m_list.addDevice(&newdev);
×
313
  newdev.setParent(this);
×
314
}
×
315

316
void DeviceDocumentPlugin::setupConnections(
80✔
317
    Device::DeviceInterface& device, bool enabled)
318
{
319
  auto& vec = m_connections[&device];
80✔
320
  if(enabled)
80✔
321
  {
322
    vec.push_back(
40✔
323
        con(device, &Device::DeviceInterface::pathAdded, this,
80✔
324
            [&, ptr = QPointer{&device}](const State::Address& addr) {
87✔
325
      if(!ptr)
47✔
326
        return;
×
327
      // FIXME A subtle bug is introduced if we want to add the root
328
      // node...
329
      if(addr.path.size() > 0)
47✔
330
      {
331
        auto parentAddr = addr;
47✔
332
        parentAddr.path.removeLast();
47✔
333

334
        Device::Node* parent = Device::try_getNodeFromAddress(m_rootNode, parentAddr);
47✔
335
        if(parent)
47✔
336
        {
337
          const auto& last = addr.path[addr.path.size() - 1];
×
338
          auto it = ossia::find_if(
×
339
              *parent, [&](const auto& n) { return n.displayName() == last; });
×
340
          if(it == parent->cend())
×
341
          {
342
            updateProxy.addLocalNode(*parent, device.getNodeWithoutChildren(addr));
×
343
          }
×
344
          else
345
          {
346
            // TODO update the node with the new information
347
          }
348
        }
×
349
      }
47✔
350
    }, Qt::QueuedConnection));
87✔
351

352
    vec.push_back(con(
80✔
353
        device, &Device::DeviceInterface::pathRemoved, this,
40✔
354
        [&](const State::Address& addr) { updateProxy.removeLocalNode(addr); },
638✔
355
        Qt::QueuedConnection));
40✔
356
    vec.push_back(con(
80✔
357
        device, &Device::DeviceInterface::pathUpdated, this,
40✔
358
        [&](const State::Address& addr, const Device::AddressSettings& set) {
517✔
359
      updateProxy.updateLocalSettings(addr, set, device);
477✔
360
        },
477✔
361
        Qt::QueuedConnection));
40✔
362
  }
40✔
363
  else
364
  {
365
    for(auto& q : vec)
160✔
366
    {
367
      QObject::disconnect(q);
120✔
368
    }
369
    m_connections.erase(&device);
40✔
370
  }
371
}
80✔
372

373
int index(const DeviceDocumentPlugin& model, const QString& name)
×
374
{
375
  for(int i = 0; i < model.rootNode().childCount(); i++)
×
376
  {
377

378
    const Device::Node& n = model.rootNode().childAt(i);
×
379
    if(n.is<Device::DeviceSettings>())
×
380
    {
381
      auto& d = n.get<Device::DeviceSettings>();
×
382
      if(d.name == name)
×
383
        return i;
×
384
    }
×
385
  }
×
386
  return -1;
×
387
}
×
388

389
void DeviceDocumentPlugin::reconnect(const QString& device)
×
390
{
391
  auto f = [this](Device::DeviceInterface& dev) {
×
392
    dev.reconnect();
×
393
    QTimer::singleShot(500, [this, &dev] {
×
394
      auto new_node = dev.refresh();
×
395

396
      auto device_index = index(*this, dev.settings().name);
×
397
      if(device_index != -1)
×
398
      {
399
        auto cmd = new Explorer::Command::ReplaceDevice{
×
400
            *this, device_index, std::move(new_node)};
×
401

402
        CommandDispatcher<>{this->context().commandStack}.submit(cmd);
×
403
      }
×
404
    });
×
405
  };
×
406

407
  if(device.isEmpty())
×
408
  {
409
    m_list.apply([&, f](Device::DeviceInterface& dev) {
×
410
      if(!dev.connected())
×
411
      {
412
        f(dev);
×
413
      }
×
414
    });
×
415
  }
×
416
  else
417
  {
418
    m_list.apply([&](Device::DeviceInterface& dev) {
×
419
      if(!dev.connected() && dev.settings().name == device)
×
420
        f(dev);
×
421
    });
×
422
  }
423
}
×
424

425
void DeviceDocumentPlugin::on_valueUpdated(
×
426
    const State::Address& addr, const ossia::value& v)
427
{
428
  ossia::qt::run_async(this, [this, aa = State::AddressAccessor{addr}, v] {
×
429
    updateProxy.updateLocalValue(aa, v);
×
430
  });
×
431
}
×
432

433
}
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