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

ossia / score / 33834678478

04 Sep 2026 03:50AM UTC coverage: 24.333% (+0.3%) from 23.997%
33834678478

push

github

jcelerier
tests: the editor-look cases assumed a Windows desktop

Three of them failed under the coverage job's Xvfb display.

Redo is Ctrl+Y only on Windows -- qplatformtheme.cpp binds that one under
KB_Win, and X11 and macOS use Ctrl+Shift+Z -- so the redo half of the hex
undo case tested nothing there. Ask QKeySequence instead.

The other two turn on an editor committing when the focus leaves it, and
QWidget::setFocus only routes through setFocusWidget in an active window.
There is no window manager on that display, so the view was shown but never
activated and no focus event was ever sent: the value simply stayed put.
The fixture now activates the window and waits for it, and the cases that
depend on it say so -- one of them was passing on an empty check.

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

55424 of 227776 relevant lines covered (24.33%)

63160.69 hits per line

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

41.01
/src/plugins/score-plugin-deviceexplorer/Explorer/Explorer/DeviceExplorerModel.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 "DeviceExplorerModel.hpp"
4

5
#include <State/MessageListSerialization.hpp>
6
#include <State/ValueConversion.hpp>
7

8
#include <Device/Address/AddressSettings.hpp>
9
#include <Device/ItemModels/NodeBasedItemModel.hpp>
10
#include <Device/ItemModels/NodeDisplayMethods.hpp>
11
#include <Device/Node/DeviceNode.hpp>
12
#include <Device/Node/NodeListMimeSerialization.hpp>
13
#include <Device/Protocol/DeviceSettings.hpp>
14
#include <Device/Protocol/ProtocolFactoryInterface.hpp>
15
#include <Device/Protocol/ProtocolList.hpp>
16

17
#include <Explorer/Commands/Add/LoadDevice.hpp>
18
#include <Explorer/Commands/Update/UpdateAddressSettings.hpp>
19
#include <Explorer/DeviceList.hpp>
20
#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
21
#include <Explorer/DocumentPlugin/NodeUpdateProxy.hpp>
22
#include <Explorer/Explorer/DeviceExplorerMimeTypes.hpp>
23
#include <Explorer/Explorer/DeviceExplorerView.hpp>
24
#include <Explorer/Explorer/Widgets/DeviceEditDialog.hpp> // TODO why here??!!
25

26
#include <score/application/ApplicationContext.hpp>
27
#include <score/application/GUIApplicationContext.hpp>
28
#include <score/command/CommandStackFacade.hpp>
29
#include <score/document/DocumentContext.hpp>
30
#include <score/document/DocumentInterface.hpp>
31
#include <score/model/path/Path.hpp>
32
#include <score/model/tree/TreeNode.hpp>
33
#include <score/model/tree/TreeNodeSerialization.hpp>
34
#include <score/plugins/InterfaceList.hpp>
35
#include <score/plugins/StringFactoryKey.hpp>
36
#include <score/serialization/JSONVisitor.hpp>
37
#include <score/serialization/MimeVisitor.hpp>
38
#include <score/widgets/MessageBox.hpp>
39

40
#include <ossia/detail/ssize.hpp>
41
#include <ossia/network/base/node_attributes.hpp>
42
#include <ossia/network/common/destination_qualifiers.hpp>
43
#include <ossia/network/domain/domain.hpp>
44

45
#include <ossia-qt/name_utils.hpp>
46

47
#include <QAbstractProxyModel>
48
#include <QApplication>
49
#include <QMainWindow>
50
#include <QMap>
51
#include <QMimeData>
52
#include <QObject>
53
#include <QTreeView>
54

55
#include <wobjectimpl.h>
56

57
#include <iostream>
58
#include <string>
59
#include <vector>
60
W_OBJECT_IMPL(Explorer::DeviceExplorerModel)
18,444✔
61
namespace Explorer
62
{
63
static const QMap<Explorer::Column, QString> HEADERS{
118✔
64
    {Explorer::Column::Name, QObject::tr("Address")},
118✔
65
    {Explorer::Column::Value, QObject::tr("Value")}};
118✔
66

67
DeviceExplorerModel::DeviceExplorerModel(DeviceDocumentPlugin& plug, QObject* parent)
192✔
68
    : NodeBasedItemModel{parent}
192✔
69
    , m_devicePlugin{plug}
192✔
70
    , m_rootNode{plug.rootNode()}
192✔
71
    , m_cmdQ{plug.context().commandStack}
192✔
72
{
192✔
73
  this->setObjectName("DeviceExplorerModel");
192✔
74

75
  beginResetModel();
192✔
76
  endResetModel();
192✔
77
}
×
78

79
DeviceExplorerModel::~DeviceExplorerModel() { }
384✔
80

81
DeviceDocumentPlugin& DeviceExplorerModel::deviceModel() const
2,869✔
82
{
83
  return m_devicePlugin;
2,869✔
84
}
85

86
QModelIndexList DeviceExplorerModel::selectedIndexes() const
×
87
{
88
  if(!m_view)
×
89
  {
90
    return {};
×
91
  }
92
  else
93
  {
94
    // We have to do this check if we have a proxy
95
    if(m_view->hasProxy())
×
96
    {
97
      auto indexes = m_view->selectedIndexes();
×
98
      for(auto& index : indexes)
×
99
        index = static_cast<const QAbstractProxyModel*>(m_view->QTreeView::model())
×
100
                    ->mapToSource(index);
×
101
      return indexes;
×
102
    }
×
103
    else
104
    {
105
      return m_view->selectedIndexes();
×
106
    }
107
  }
108
}
×
109

110
QStringList DeviceExplorerModel::getColumns() const
112✔
111
{
112
  return HEADERS.values();
112✔
113
}
114

115
int DeviceExplorerModel::addDevice(const Device::Node& deviceNode)
×
116
{
117
  int row = m_rootNode.childCount();
×
118
  QModelIndex parent; // invalid
×
119

120
  beginInsertRows(parent, row, row);
×
121
  rootNode().push_back(deviceNode);
×
122
  endInsertRows();
×
123

124
  return row;
×
125
}
126

127
int DeviceExplorerModel::addDevice(Device::Node&& deviceNode)
60✔
128
{
129
  deviceNode.setParent(&rootNode());
60✔
130

131
  int row = m_rootNode.childCount();
60✔
132
  QModelIndex parent; // invalid
60✔
133

134
  beginInsertRows(parent, row, row);
60✔
135
  rootNode().push_back(std::move(deviceNode));
60✔
136
  endInsertRows();
60✔
137

138
  return row;
60✔
139
}
140

141
bool DeviceExplorerModel::replaceDevice(Device::Node&& deviceNode)
14✔
142
{
143
  const auto& name = deviceNode.get<Device::DeviceSettings>().name;
14✔
144
  int row = 0;
14✔
145
  for(auto& n : m_rootNode)
14✔
146
  {
147
    if(n.get<Device::DeviceSettings>().name == name)
14✔
148
    {
149
      const QModelIndex index = createIndex(row, 0, &n);
14✔
150

151
      deviceTreeAboutToChange(&n);
14✔
152

153
      // Out with the old children...
154
      if(const int count = n.childCount(); count > 0)
14✔
155
      {
156
        beginRemoveRows(index, 0, count - 1);
14✔
157
        n.takeChildren();
14✔
158
        endRemoveRows();
14✔
159
      }
14✔
160

161
      // ... update the settings ...
162
      n.set(deviceNode.get<Device::DeviceSettings>());
14✔
163
      dataChanged(index, createIndex(row, columnCount() - 1, &n));
14✔
164

165
      // ... and in with the new.
166
      if(const int count = deviceNode.childCount(); count > 0)
14✔
167
      {
168
        beginInsertRows(index, 0, count - 1);
14✔
169
        deviceNode.moveChildren(n);
14✔
170
        endInsertRows();
14✔
171
      }
14✔
172

173
      deviceTreeChanged(&n);
14✔
174
      return true;
14✔
175
    }
176
    row++;
×
177
  }
178
  return false;
×
179
}
14✔
180

181
void DeviceExplorerModel::updateDevice(
26✔
182
    const QString& name, const Device::DeviceSettings& dev)
183
{
184
  int i = 0;
26✔
185
  for(auto& n : m_rootNode)
26✔
186
  {
187
    if(n.get<Device::DeviceSettings>().name == name)
26✔
188
    {
189
      n.set(dev);
26✔
190

191
      QModelIndex index = createIndex(i, 0, &n);
26✔
192
      dataChanged(index, index);
26✔
193
      return;
26✔
194
    }
195
    i++;
×
196
  }
197
}
26✔
198

199
Device::Node* DeviceExplorerModel::addAddress(
×
200
    Device::Node* parentNode, const Device::AddressSettings& addressSettings, int row)
201
{
202
  SCORE_ASSERT(parentNode);
×
203
  SCORE_ASSERT(parentNode != &m_rootNode);
×
204

205
  Device::Node* grandparent = parentNode->parent();
×
206
  SCORE_ASSERT(grandparent);
×
207
  int rowParent = grandparent->indexOfChild(parentNode);
×
208
  QModelIndex parentIndex = createIndex(rowParent, 0, parentNode);
×
209

210
  beginInsertRows(parentIndex, row, row);
×
211

212
  auto it = parentNode->begin();
×
213
  std::advance(it, row);
×
214

215
  auto& ret = parentNode->emplace(it, addressSettings, parentNode);
×
216

217
  endInsertRows();
×
218

219
  return &ret;
×
220
}
221

222
void DeviceExplorerModel::addNode(
2✔
223
    Device::Node* parentNode, Device::Node&& child, int row)
224
{
225
  SCORE_ASSERT(parentNode);
2✔
226
  SCORE_ASSERT(parentNode != &m_rootNode);
2✔
227

228
  Device::Node* grandparent = parentNode->parent();
2✔
229
  SCORE_ASSERT(grandparent);
2✔
230
  int rowParent = grandparent->indexOfChild(parentNode);
2✔
231
  QModelIndex parentIndex = createIndex(rowParent, 0, parentNode);
2✔
232

233
  beginInsertRows(parentIndex, row, row);
2✔
234

235
  auto it = parentNode->begin();
2✔
236
  std::advance(it, row);
2✔
237
  parentNode->emplace(it, std::move(child));
2✔
238

239
  endInsertRows();
2✔
240
}
2✔
241

242
void DeviceExplorerModel::updateAddress(
10✔
243
    Device::Node* node, const Device::AddressSettings& addressSettings)
244
{
245
  SCORE_ASSERT(node);
10✔
246
  SCORE_ASSERT(node != &m_rootNode);
10✔
247

248
  node->set(addressSettings);
10✔
249

250
  nodeChanged(node);
10✔
251

252
  dataChanged(
10✔
253
      modelIndexFromNode(*node, 0), modelIndexFromNode(*node, (int)Column::Count - 1));
10✔
254
}
10✔
255

256
void DeviceExplorerModel::updateValue(
24✔
257
    Device::Node* n, const State::AddressAccessor& addr, const ossia::value& v)
258
{
259
  if(!addr.qualifiers.get().accessors.empty())
24✔
260
  {
261
    SCORE_TODO;
×
262
  }
×
263
  n->get<Device::AddressSettings>().value = v;
24✔
264

265
  QModelIndex nodeIndex = modelIndexFromNode(*n, 1);
24✔
266

267
  dataChanged(nodeIndex, nodeIndex);
24✔
268
  valueUpdated(n);
24✔
269
}
24✔
270

271
bool DeviceExplorerModel::checkDeviceInstantiatable(
17✔
272
    const Device::DeviceSettings& n) const
273
{
274
  // No name -> no love
275
  if(n.name.isEmpty())
17✔
276
    return false;
×
277

278
  // Request from the protocol factory the protocol to see
279
  // if it is compatible.
280
  auto& context = m_devicePlugin.context().app;
17✔
281
  auto prot = context.interfaces<Device::ProtocolFactoryList>().get(n.protocol);
17✔
282
  if(!prot)
17✔
283
    return false;
17✔
284

285
  // Look for other childs in the same protocol.
286
  bool none_incompatible = std::none_of(
×
287
      rootNode().begin(), rootNode().end(), [&](const Device::Node& child) {
×
288
    SCORE_ASSERT(child.is<Device::DeviceSettings>());
×
289
    const auto& set = child.get<Device::DeviceSettings>();
×
290
    return (set.name == n.name)
×
291
           || (set.protocol
×
292
                   == n.protocol // FIXME distinct protocols could use the same port...
×
293
               && !prot->checkCompatibility(n, child.get<Device::DeviceSettings>()));
×
294
  });
295
  if(!none_incompatible)
×
296
    return false;
×
297

298
  // Check compatibility with an empty device
299
  return prot->checkCompatibility(n, Device::DeviceSettings{});
×
300
}
17✔
301

302
bool DeviceExplorerModel::checkDeviceEditable(
3✔
303
    const QString& originalName, const Device::DeviceSettings& n) const
304
{
305
  // No name -> no love
306
  if(n.name.isEmpty())
3✔
307
    return false;
×
308

309
  // Request from the protocol factory the protocol to see
310
  // if it is compatible.
311
  auto& context = m_devicePlugin.context().app;
3✔
312
  auto prot = context.interfaces<Device::ProtocolFactoryList>().get(n.protocol);
3✔
313
  if(!prot)
3✔
314
    return false;
3✔
315

316
  // Look for other childs in the same protocol.
317
  bool none_incompatible = std::none_of(
×
318
      rootNode().begin(), rootNode().end(), [&](const Device::Node& child) {
×
319
    SCORE_ASSERT(child.is<Device::DeviceSettings>());
×
320
    const auto& set = child.get<Device::DeviceSettings>();
×
321
    // Ignore the device that is being edited
322
    if(set.name == originalName)
×
323
      return false;
×
324

325
    return (set.name == n.name)
×
326
           || (set.protocol == n.protocol
×
327
               && !prot->checkCompatibility(n, child.get<Device::DeviceSettings>()));
×
328
  });
×
329
  if(!none_incompatible)
×
330
    return false;
×
331

332
  // Check compatibility with an empty device
333
  return prot->checkCompatibility(n, Device::DeviceSettings{});
×
334
}
3✔
335

336
bool DeviceExplorerModel::checkAddressInstantiatable(
×
337
    Device::Node& parent, const Device::AddressSettings& addr)
338
{
339
  SCORE_ASSERT(!parent.is<InvisibleRootNode>());
×
340

341
  if(addr.name.isEmpty())
×
342
    return false;
×
343

344
  return std::none_of(parent.begin(), parent.end(), [&](const Device::Node& n) {
×
345
    return n.get<Device::AddressSettings>().name == addr.name;
×
346
  });
347
}
×
348

349
bool DeviceExplorerModel::checkAddressEditable(
6✔
350
    Device::Node& parent, const Device::AddressSettings& before,
351
    const Device::AddressSettings& after)
352
{
353
  SCORE_ASSERT(!parent.is<InvisibleRootNode>());
6✔
354

355
  if(after.name.isEmpty())
6✔
356
    return false;
×
357

358
  auto it = std::find_if(parent.begin(), parent.end(), [&](const Device::Node& n) {
17✔
359
    return n.get<Device::AddressSettings>().name == after.name;
11✔
360
  });
361
  if(it != parent.end())
6✔
362
  {
363
    //  We didn't change name, it's ok
364
    if(after.name == before.name)
4✔
365
      return true;
3✔
366
    else
367
      return false;
1✔
368
  }
369
  else
370
  {
371
    // Ok, no conflicts
372
    return true;
2✔
373
  }
374
}
6✔
375

376
bool DeviceExplorerModel::canRenameNode(const Device::Node& n) const
4,399✔
377
{
378
  if(!n.is<Device::AddressSettings>())
4,399✔
379
    return false;
×
380

381
  // Called for every visible cell: walk up to the device rather than building
382
  // the whole address just to read its first component.
383
  const Device::Node* root = &n;
4,399✔
384
  while(root->parent() && !root->is<Device::DeviceSettings>())
8,798✔
385
    root = root->parent();
4,399✔
386

387
  if(!root->is<Device::DeviceSettings>())
4,399✔
388
    return false;
×
389

390
  auto* dev = m_devicePlugin.list().findDevice(root->get<Device::DeviceSettings>().name);
4,399✔
391
  return dev && dev->capabilities().canRenameNode;
4,399✔
392
}
4,399✔
393

394
int DeviceExplorerModel::columnCount() const
126✔
395
{
396
  return (int)Column::Count;
126✔
397
}
398

399
int DeviceExplorerModel::columnCount(const QModelIndex& /*parent*/) const
6,042✔
400
{
401
  return (int)Column::Count;
6,042✔
402
}
403

404
// must return an invalid QVariant for cases not handled
405
QVariant DeviceExplorerModel::data(const QModelIndex& index, int role) const
46,299✔
406
{
407
  const int col = index.column();
46,299✔
408

409
  if(col < 0 || col >= (int)Column::Count)
46,299✔
410
  {
411
    return QVariant();
×
412
  }
413

414
  const Device::Node& n = nodeFromModelIndex(index);
46,299✔
415
  if(role != Qt::ToolTipRole)
46,299✔
416
  {
417
    switch((Column)col)
46,299✔
418
    {
419
      case Column::Name: {
420
        if(n.is<Device::AddressSettings>())
24,591✔
421
          return Device::nameColumnData(n, role);
22,181✔
422
        else if(n.is<Device::DeviceSettings>())
2,410✔
423
        {
424
          auto& dev_set = n.get<Device::DeviceSettings>();
2,410✔
425
          return Device::deviceNameColumnData(
2,410✔
426
              n, deviceModel().list().device(dev_set.name).connected(), role);
2,410✔
427
        }
428
        return {};
×
429
      }
430

431
      case Column::Value:
432
        return Device::valueColumnData(n, role);
21,708✔
433

434
      case Column::Count:
×
435
      default:
436
        SCORE_ABORT;
×
437
        return {};
×
438
    }
439
  }
440
  else
441
  {
442
    // Tooltip
443
    if(n.is<Device::AddressSettings>())
×
444
    {
445
      auto& addr_set = n.get<Device::AddressSettings>();
×
446

447
      // What the one-line cell had to fold away.
448
      if((Column)col == Column::Value)
×
449
      {
450
        if(const auto* s = addr_set.value.target<std::string>())
×
451
        {
452
          const auto tip
453
              = State::convert::stringCellToolTip(QByteArray::fromStdString(*s));
×
454
          if(!tip.isEmpty())
×
455
            return tip;
×
456
        }
×
457
      }
×
458

459
      if(const auto& desc = ossia::net::get_description(addr_set))
×
460
        return QString::fromStdString(*desc);
×
461
    }
×
462
    else
463
    {
464
      return {};
×
465
    }
466
  }
467
  return {};
×
468
}
46,299✔
469

470
QVariant
471
DeviceExplorerModel::headerData(int section, Qt::Orientation orientation, int role) const
2,671✔
472
{
473
  if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
2,671✔
474
  {
475
    if(section >= 0 && section < (int)Column::Count)
751✔
476
    {
477
      return HEADERS[(Column)section];
751✔
478
    }
479
  }
×
480

481
  return {};
1,920✔
482
}
2,671✔
483

484
Qt::ItemFlags DeviceExplorerModel::flags(const QModelIndex& index) const
8,519✔
485
{
486
  Qt::ItemFlags f = Qt::ItemIsEnabled;
8,519✔
487
  // by default QAbstractItemModel::flags(index); returns Qt::ItemIsEnabled |
488
  // Qt::ItemIsSelectable
489

490
  if(index.isValid())
8,519✔
491
  {
492
    const Device::Node& n = nodeFromModelIndex(index);
8,519✔
493

494
    if(n.isSelectable())
8,519✔
495
    {
496
      f |= Qt::ItemIsSelectable;
8,519✔
497
    }
8,519✔
498

499
    // we allow drag'n drop only from the name column
500
    if(index.column() == (int)Column::Name)
8,519✔
501
    {
502
      f |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
5,072✔
503
    }
5,072✔
504

505
    if(n.isEditable() && index.column() == (int)Column::Value)
8,519✔
506
    {
507
      // A boolean toggles on one click and an impulse is a bang painted in the
508
      // cell: what the row draws *is* the editor in both cases, so the row is
509
      // not editable on top of it.
510
      const auto* addr = n.target<Device::AddressSettings>();
3,144✔
511
      const bool boolean = addr && addr->value.target<bool>();
3,144✔
512
      const bool impulse
3,144✔
513
          = addr && addr->value.get_type() == ossia::val_type::IMPULSE;
3,144✔
514

515
      if(boolean)
3,144✔
516
        f |= Qt::ItemIsUserCheckable;
285✔
517
      else if(!impulse)
2,859✔
518
        f |= Qt::ItemIsEditable;
2,572✔
519
    }
3,144✔
520

521
    if(index.column() == (int)Column::Name && n.is<Device::AddressSettings>()
8,519✔
522
       && canRenameNode(n))
5,072✔
523
    {
524
      f |= Qt::ItemIsEditable;
4,395✔
525
    }
4,395✔
526
  }
8,519✔
527
  else
528
  {
529
    // to be able to drop even where there is nothing
530
    f |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
×
531
  }
532

533
  return f;
8,519✔
534
}
535

536
/*
537
  return false if no change was made.
538
  dataChanged() & return true if a change is made.
539

540
  Note: this is the function that gets called when the user changes the value
541
  in the tree.
542
  It then sends a command that calls editData.
543
*/
544
bool DeviceExplorerModel::setData(
26✔
545
    const QModelIndex& index, const QVariant& value, int role)
546
{
547
  if(!index.isValid())
26✔
548
    return false;
×
549

550
  auto& n = nodeFromModelIndex(index);
26✔
551

552
  if(!n.is<Device::AddressSettings>())
26✔
553
    return false;
×
554

555
  auto col = Explorer::Column(index.column());
26✔
556

557
  if(role == Qt::EditRole || role == Qt::CheckStateRole)
26✔
558
  {
559
    if(col == Column::Value)
26✔
560
    {
561
      // In this case we don't make a command, but we directly push the
562
      // new value.
563
      // A check state is a tri-state enum, not a number: read it as the
564
      // boolean it stands for rather than leaving Qt::Checked == 2 to convert.
565
      auto copy = role == Qt::CheckStateRole
45✔
566
                      ? ossia::value{value.value<Qt::CheckState>() == Qt::Checked}
1✔
567
                  : value.canConvert<ossia::value>()
22✔
568
                      ? value.value<ossia::value>()
22✔
569
                      : State::convert::fromQVariant(value);
×
570

571
      // We may have to convert types.
572
      const ossia::value& orig = n.get<Device::AddressSettings>().value;
23✔
573
      if(copy.v.which() != orig.v.which() && !State::convert::convert(orig, copy))
23✔
574
        return false;
×
575

576
      n.get<Device::AddressSettings>().value = copy;
23✔
577

578
      // Note : if we want to disable remote updating, we have to do it
579
      // here (e.g. if this becomes a settings)
580
      m_devicePlugin.updateProxy.updateRemoteValue(Device::address(n).address, copy);
23✔
581

582
      dataChanged(index, index);
23✔
583
      valueUpdated(&n);
23✔
584
      return true;
23✔
585
    }
23✔
586
    else
587
    {
588
      // Here we make a command because we change the structure of the tree.
589
      auto settings = n.get<Device::AddressSettings>();
3✔
590
      if(col == Column::Name)
3✔
591
      {
592
        const QString s = value.toString();
3✔
593
        auto* parent = n.parent();
3✔
594
        if(s.isEmpty() || !parent || !canRenameNode(n))
3✔
595
          return false;
1✔
596

597
        auto renamed = n.get<Device::AddressSettings>();
2✔
598
        renamed.name = s;
2✔
599

600
        if(!checkAddressEditable(*parent, n.get<Device::AddressSettings>(), renamed))
2✔
601
          return false;
1✔
602

603
        settings.name = s;
1✔
604
      }
3✔
605

606
      if(settings != n.get<Device::AddressSettings>())
1✔
607
      {
608
        // We changed
609
        m_cmdQ.redoAndPush(new Explorer::Command::UpdateAddressSettings{
2✔
610
            this->deviceModel(), Device::NodePath{n}, settings});
1✔
611
        return true;
1✔
612
      }
613
    }
3✔
614
  }
×
615

616
  return false;
×
617
}
26✔
618

619
bool DeviceExplorerModel::setHeaderData(int, Qt::Orientation, const QVariant&, int)
×
620
{
621
  return false; // we prevent editing the (column) headers
×
622
}
623

624
/**
625
 * @brief DeviceExplorerModel::editData
626
 *
627
 * This functions gets called by the command
628
 * that edit the columns.
629
 */
630
void DeviceExplorerModel::editData(
×
631
    const Device::NodePath& path, Explorer::Column column, const ossia::value& value,
632
    int role)
633
{
634
  Device::Node* node = path.toNode(&rootNode());
×
635
  editData(*node, column, value, role);
×
636
}
×
637

638
void DeviceExplorerModel::editData(
1✔
639
    Device::Node& node, Column column, const ossia::value& value, int role)
640
{
641
  SCORE_ASSERT(node.parent());
1✔
642

643
  if(node.is<Device::DeviceSettings>())
1✔
644
    return;
×
645

646
  if(role == Qt::EditRole && column == Column::Value)
1✔
647
  {
648
    node.get<Device::AddressSettings>().value = value;
1✔
649
  }
1✔
650

651
  const QModelIndex index = modelIndexFromNode(node, (int)column);
1✔
652
  dataChanged(index, index);
1✔
653
}
1✔
654

655
QModelIndex DeviceExplorerModel::bottomIndex(const QModelIndex& index) const
×
656
{
657
  auto& node = nodeFromModelIndex(index);
×
658

659
  if(!node.hasChildren())
×
660
  {
661
    return index;
×
662
  }
663

664
  return bottomIndex(createIndex(
×
665
      node.childCount() - 1, index.column(), &node.childAt(node.childCount() - 1)));
×
666
}
×
667

668
bool DeviceExplorerModel::isDevice(QModelIndex index) const
×
669
{
670
  if(!index.isValid())
×
671
  {
672
    return false;
×
673
  }
674

675
  const Device::Node& n = nodeFromModelIndex(index);
×
676
  return n.is<Device::DeviceSettings>();
×
677
}
×
678

679
bool DeviceExplorerModel::isEmpty() const
225✔
680
{
681
  return m_rootNode.childCount() == 0;
225✔
682
}
683

684
/*
685
Drag and drop works by deleting the dragged items and creating a new set of
686
dropped items that match those dragged.
687
I will/may call insertRows(), removeRows(), dropMimeData(), ...
688
We define two MimeTypes : address and device.
689
It allows to distinguish whether we are drag'n dropping devices or addresses.
690
 */
691

692
Qt::DropActions DeviceExplorerModel::supportedDropActions() const
×
693
{
694
  return (Qt::CopyAction);
×
695
}
696

697
// Default supportedDragActions() implementation returns
698
// supportedDropActions().
699

700
Qt::DropActions DeviceExplorerModel::supportedDragActions() const
×
701
{
702
  return (Qt::CopyAction);
×
703
}
704

705
QStringList DeviceExplorerModel::mimeTypes() const
×
706
{
707
  return {score::mime::device(), score::mime::address()};
×
708
}
×
709

710
SelectedNodes
711
DeviceExplorerModel::uniqueSelectedNodes(const QModelIndexList& indexes) const
×
712
{
713
  SelectedNodes nodes;
×
714
  ossia::transform(
×
715
      indexes, std::back_inserter(nodes.parents),
×
716
      [&](const QModelIndex& idx) { return &nodeFromModelIndex(idx); });
×
717

718
  ossia::remove_erase(nodes.parents, &m_rootNode);
×
719

720
  nodes.messages.reserve(nodes.messages.size() + nodes.parents.size());
×
721
  if(nodes.parents.size() > 1)
×
722
  {
723
    // Filter messages
724
    for(auto node : nodes.parents)
×
725
    {
726
      if(node->is<Device::AddressSettings>())
×
727
      {
728
        auto& val = node->get<Device::AddressSettings>();
×
729
        if(val.ioType == ossia::access_mode::SET)
×
730
        {
731
          nodes.messages.push_back(node);
×
732
        }
×
733
      }
×
734
    }
735

736
    // Filter parents
737
    nodes.parents = filterUniqueParents(nodes.parents);
×
738
  }
×
739
  else if(nodes.parents.size() == 1)
×
740
  {
741
    Device::Node* node = nodes.parents[0];
×
742
    if(node->is<Device::AddressSettings>() && node->childCount() == 0)
×
743
    {
744
      // If a single node is selected we copy it even if it is "get"
745
      nodes.messages = std::move(nodes.parents);
×
746
    }
×
747
  }
×
748

749
  return nodes;
×
750
}
×
751
// method called when a drag is initiated
752
QMimeData* DeviceExplorerModel::mimeData(const QModelIndexList& indexes) const
×
753
{
754
  auto uniqueNodes = uniqueSelectedNodes(indexes);
×
755

756
  // Handle case of a single device which can have custom mimeData
757
  if(uniqueNodes.parents.size() == 1
×
758
     && uniqueNodes.parents[0]->is<Device::DeviceSettings>())
×
759
  {
760
    auto node = uniqueNodes.parents[0];
×
761
    if(node->is<Device::DeviceSettings>())
×
762
    {
763
      auto& dev = deviceModel().list().device(node->get<Device::DeviceSettings>().name);
×
764
      if(auto d = dev.mimeData())
×
765
        return d;
×
766
    }
×
767
  }
×
768

769
  auto mimeData = new QMimeData;
×
770

771
  // Now we request an update to the device explorer.
772
  m_devicePlugin.updateProxy.refreshRemoteValues(uniqueNodes.parents);
×
773

774
  // The "MessagesList" part.
775
  State::MessageList messages;
×
776
  messages.reserve(uniqueNodes.parents.size() + uniqueNodes.messages.size());
×
777
  for(const auto& node : uniqueNodes.parents)
×
778
  {
779
    Device::parametersList(*node, messages);
×
780
  }
781

782
  for(const auto& node : uniqueNodes.messages)
×
783
  {
784
    messages.push_back(Device::message(*node));
×
785
  }
786

787
  if(!messages.empty())
×
788
  {
789
    Mime<State::MessageList>::Serializer s{*mimeData};
×
790
    s.serialize(messages);
×
791
  }
×
792

793
  // The "Nodes" part. Deserialize with FreeNodeList to get the addresses.
794
  {
795
    Mime<Device::NodeList>::Serializer s{*mimeData};
×
796
    Device::NodeList vec;
×
797
    vec.reserve(uniqueNodes.parents.size() + uniqueNodes.messages.size());
×
798
    vec.insert(vec.end(), uniqueNodes.parents.begin(), uniqueNodes.parents.end());
×
799
    vec.insert(vec.end(), uniqueNodes.messages.begin(), uniqueNodes.messages.end());
×
800
    s.serialize(vec);
×
801
  }
×
802

803
  if(messages.empty() && uniqueNodes.parents.empty() && uniqueNodes.messages.empty())
×
804
  {
805
    delete mimeData;
×
806
    return nullptr;
×
807
  }
808

809
  return mimeData;
×
810
}
×
811

812
bool DeviceExplorerModel::canDropMimeData(
×
813
    const QMimeData* mimeData, Qt::DropAction action, int /*row*/, int /*column*/,
814
    const QModelIndex& parent) const
815
{
816
  if(action == Qt::IgnoreAction)
×
817
  {
818
    return true;
×
819
  }
820

821
  if(action != Qt::MoveAction && action != Qt::CopyAction)
×
822
  {
823
    return false;
×
824
  }
825

826
  if(!mimeData
×
827
     || (!mimeData->hasFormat(score::mime::device())
×
828
         && !mimeData->hasFormat(score::mime::address())))
×
829
  {
830
    return false;
×
831
  }
832

833
  const Device::Node& parentNode = nodeFromModelIndex(parent);
×
834

835
  if(mimeData->hasFormat(score::mime::address()))
×
836
  {
837
    if(&parentNode == &m_rootNode)
×
838
    {
839
      return false;
×
840
    }
841
  }
×
842
  else
843
  {
844
    SCORE_ASSERT(mimeData->hasFormat(score::mime::device()));
×
845

846
    if(&parentNode != &m_rootNode)
×
847
    {
848
      return false;
×
849
    }
850
  }
851

852
  return true;
×
853
}
×
854

855
// method called when a drop occurs
856
// return true if drop really handled, false otherwise.
857
//
858
// if dropMimeData returns true && action==Qt::MoveAction, removeRows is called
859
// immediately after
860
bool DeviceExplorerModel::dropMimeData(
×
861
    const QMimeData* mimeData, Qt::DropAction action, int row, int column,
862
    const QModelIndex& parent)
863
{
864
  if(action == Qt::IgnoreAction)
×
865
  {
866
    return true;
×
867
  }
868

869
  if(action != Qt::MoveAction && action != Qt::CopyAction)
×
870
  {
871
    return false;
×
872
  }
873

874
  if(!mimeData
×
875
     || (!mimeData->hasFormat(score::mime::device())
×
876
         && !mimeData->hasFormat(score::mime::address())))
×
877
  {
878
    return false;
×
879
  }
880

881
  QModelIndex parentIndex; // invalid
×
882
  Device::Node* parentNode = &m_rootNode;
×
883
  QString mimeType = score::mime::device();
×
884

885
  if(mimeData->hasFormat(score::mime::address()))
×
886
  {
887
    parentIndex = parent;
×
888
    parentNode = &nodeFromModelIndex(parent);
×
889
    mimeType = score::mime::address();
×
890

891
    if(parentNode == &m_rootNode)
×
892
    {
893
      return false;
×
894
    }
895
  }
×
896
  else
897
  {
898
    SCORE_ASSERT(mimeData->hasFormat(score::mime::device()));
×
899
    SCORE_ASSERT(mimeType == score::mime::device());
×
900
  }
901

902
  if(parentNode)
×
903
  {
904
    // Note : when dropping a device,
905
    // if there is an existing device that would use the same ports, etc.
906
    // we have to open a dialog to change the device settings.
907

908
    const auto& json = readJson(mimeData->data(mimeType));
×
909
    JSONObject::Deserializer deser{json};
×
910
    Device::Node n;
×
911
    deser.writeTo(n);
×
912

913
    if(mimeType == score::mime::device())
×
914
    {
915
      checkAndLoadDevice(n);
×
916
    }
×
917
    return true;
×
918
  }
×
919
  return false;
×
920
}
×
921

922
void DeviceExplorerModel::checkAndLoadDevice(Device::Node n)
×
923
{
924
  SCORE_ASSERT(n.is<Device::DeviceSettings>());
×
925
  auto& deviceSettings = *n.target<Device::DeviceSettings>();
×
926

927
  // We ask the user to fix the incompatibilities by himself, maybe change
928
  // the name also.
929
  auto dialog = new DeviceEditDialog{
×
930
      *this, m_devicePlugin.context().app.interfaces<Device::ProtocolFactoryList>(),
×
931
      DeviceEditDialog::Creating, QApplication::activeWindow()};
×
932

933
  dialog->setSettings(deviceSettings);
×
934
  dialog->setBrowserEnabled(false);
×
935

936
  connect(
×
937
      dialog, &QDialog::accepted, this, [this, dialog, node = std::move(n)]() mutable {
×
938
        auto new_settings = dialog->getSettings();
×
939
        auto& deviceSettings = *node.target<Device::DeviceSettings>();
×
940
        deviceSettings = new_settings;
×
941
        if(!checkDeviceInstantiatable(deviceSettings))
×
942
        {
943
          score::warning(
×
944
              m_devicePlugin.context().app.mainWindow, "Error",
×
945
              "Could not create the device");
×
946
          dialog->deleteLater();
×
947
          return;
×
948
        }
949
        ossia::net::sanitize_device_name(deviceSettings.name);
×
950

951
        auto cmd = new Command::LoadDevice{deviceModel(), std::move(node)};
×
952
        m_cmdQ.redoAndPush(cmd);
×
953
        dialog->deleteLater();
×
954
      });
×
955
  connect(dialog, &QDialog::rejected, this, [dialog] { dialog->deleteLater(); });
×
956
  dialog->show();
×
957
}
×
958

959
void DeviceExplorerModel::checkAndLoadDevice(Device::DeviceSettings deviceSettings)
×
960
{
961
  // We ask the user to fix the incompatibilities by himself, maybe change
962
  // the name also.
963
  auto dialog = new DeviceEditDialog{
×
964
      *this, m_devicePlugin.context().app.interfaces<Device::ProtocolFactoryList>(),
×
965
      DeviceEditDialog::Creating, QApplication::activeWindow()};
×
966

967
  dialog->setSettings(deviceSettings);
×
968
  dialog->setBrowserEnabled(false);
×
969

970
  connect(dialog, &QDialog::accepted, this, [this, dialog] {
×
971
    auto node = dialog->getDevice();
×
972
    auto& deviceSettings = *node.target<Device::DeviceSettings>();
×
973
    if(!checkDeviceInstantiatable(deviceSettings))
×
974
    {
975
      score::warning(
×
976
          m_devicePlugin.context().app.mainWindow, "Error",
×
977
          "Could not create the device");
×
978
      dialog->deleteLater();
×
979
      return;
×
980
    }
981
    ossia::net::sanitize_device_name(deviceSettings.name);
×
982

983
    auto cmd = new Command::LoadDevice{deviceModel(), std::move(node)};
×
984
    m_cmdQ.redoAndPush(cmd);
×
985
    dialog->deleteLater();
×
986
  });
×
987
  connect(dialog, &QDialog::rejected, this, [dialog] { dialog->deleteLater(); });
×
988
  dialog->show();
×
989
}
×
990

991
void DeviceExplorerModel::debug_printPath(const Device::NodePath& path)
×
992
{
993
  const int pathSize = std::ssize(path);
×
994

995
  for(int i = 0; i < pathSize; ++i)
×
996
  {
997
    std::cerr << path.at(i) << " ";
×
998
  }
×
999

1000
  std::cerr << "\n";
×
1001
}
×
1002

1003
void DeviceExplorerModel::debug_printIndexes(const QModelIndexList& indexes)
×
1004
{
1005
  std::cerr << "indexes: " << indexes.size() << " nodes: \n";
×
1006
  Q_FOREACH(const QModelIndex& index, indexes)
×
1007
  {
1008
    if(index.isValid())
×
1009
    {
1010
      std::cerr << " index.row=" << index.row() << " col=" << index.column() << " ";
×
1011
      Device::Node* n = &nodeFromModelIndex(index);
×
1012
      std::cerr << " n=" << n << " ";
×
1013
      Device::Node* parent = n->parent();
×
1014

1015
      if(n == &m_rootNode)
×
1016
      {
1017
        std::cerr << " rootNode parent=" << parent << "\n";
×
1018
      }
×
1019
      else
1020
      {
1021
        std::cerr << " n->name=" << n->displayName().toStdString();
×
1022
        std::cerr << " parent=" << parent;
×
1023
        std::cerr << " parent->name=" << parent->displayName().toStdString() << "\n";
×
1024
      }
1025
    }
×
1026
    else
1027
    {
1028
      std::cerr << " invalid index \n";
×
1029
    }
1030
  }
1031
}
×
1032

1033
State::MessageList getSelectionSnapshot(DeviceExplorerModel& model)
×
1034
{
1035
  // Filter
1036
  auto uniqueNodes = model.uniqueSelectedNodes(model.selectedIndexes());
×
1037

1038
  // Recursive refresh
1039
  model.deviceModel().updateProxy.refreshRemoteValues(uniqueNodes.parents);
×
1040

1041
  // Conversion
1042
  State::MessageList messages;
×
1043
  messages.reserve(uniqueNodes.parents.size() + uniqueNodes.messages.size());
×
1044
  for(const auto& node : uniqueNodes.parents)
×
1045
  {
1046
    Device::parametersList(*node, messages);
×
1047
  }
1048
  for(const auto& node : uniqueNodes.messages)
×
1049
  {
1050
    messages.push_back(Device::message(*node));
×
1051
  }
1052

1053
  return messages;
×
1054
}
×
1055

1056
DeviceExplorerModel* try_deviceExplorerFromObject(const QObject& obj)
287✔
1057
{
1058
  auto plug = score::IDocument::documentContext(obj).findPlugin<DeviceDocumentPlugin>();
287✔
1059
  if(plug)
287✔
1060
    return &plug->explorer();
287✔
1061
  return nullptr;
×
1062
}
287✔
1063

1064
DeviceExplorerModel& deviceExplorerFromObject(const QObject& obj)
3✔
1065
{
1066
  auto expl = try_deviceExplorerFromObject(obj);
3✔
1067
  SCORE_ASSERT(expl);
3✔
1068
  return *expl;
3✔
1069
}
1070

1071
DeviceExplorerModel* try_deviceExplorerFromContext(const score::DocumentContext& ctx)
6✔
1072
{
1073
  auto plug = ctx.findPlugin<DeviceDocumentPlugin>();
6✔
1074
  if(plug)
6✔
1075
    return &plug->explorer();
6✔
1076
  return nullptr;
×
1077
}
6✔
1078

1079
DeviceExplorerModel& deviceExplorerFromContext(const score::DocumentContext& ctx)
6✔
1080
{
1081
  auto expl = try_deviceExplorerFromContext(ctx);
6✔
1082
  SCORE_ASSERT(expl);
6✔
1083
  return *expl;
6✔
1084
}
1085

1086
Device::FullAddressAccessorSettings makeFullAddressAccessorSettings(
×
1087
    const State::AddressAccessor& addr, const score::DocumentContext& ctx,
1088
    ossia::value min, ossia::value max, ossia::value val)
1089
{
1090
  // First try to find if there is a matching address
1091
  // in the device explorer
1092
  auto deviceexplorer = Explorer::try_deviceExplorerFromContext(ctx);
×
1093
  if(deviceexplorer)
×
1094
  {
1095
    return Device::makeFullAddressAccessorSettings(
×
1096
        addr, *deviceexplorer, std::move(min), std::move(max), std::move(val));
×
1097
  }
1098

1099
  // If there is none, build with some default settings
1100
  Device::FullAddressAccessorSettings s;
×
1101
  s.address = addr;
×
1102
  s.value = std::move(val);
×
1103
  s.domain = ossia::make_domain(std::move(min), std::move(max));
×
1104
  return s;
×
1105
}
×
1106
}
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