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

ossia / score / 32671464175

23 Aug 2026 10:43PM UTC coverage: 23.21% (+1.6%) from 21.606%
32671464175

push

github

jcelerier
scenario: export NodalIntervalView

test_integration_nodal_viewport and test_integration_cable_drag_view_scroll
call into the class from outside the plug-in, so a shared-plugin build could
not link either of them: the library held 136 NodalIntervalView symbols and
exported none. A static-plugin build never sees it, which is why CI does not.

51875 of 223502 relevant lines covered (23.21%)

63063.32 hits per line

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

39.71
/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)
7,035✔
61
namespace Explorer
62
{
63
static const QMap<Explorer::Column, QString> HEADERS{
88✔
64
    {Explorer::Column::Name, QObject::tr("Address")},
88✔
65
    {Explorer::Column::Value, QObject::tr("Value")}};
88✔
66

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

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

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

81
DeviceDocumentPlugin& DeviceExplorerModel::deviceModel() const
390✔
82
{
83
  return m_devicePlugin;
390✔
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
91✔
111
{
112
  return HEADERS.values();
91✔
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)
41✔
128
{
129
  deviceNode.setParent(&rootNode());
41✔
130

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

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

138
  return row;
41✔
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
}
24✔
269

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

386
  if(!root->is<Device::DeviceSettings>())
15✔
387
    return false;
×
388

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

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

398
int DeviceExplorerModel::columnCount(const QModelIndex& /*parent*/) const
408✔
399
{
400
  return (int)Column::Count;
408✔
401
}
402

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

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

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

430
      case Column::Value:
431
        return Device::valueColumnData(n, role);
×
432

433
      case Column::Count:
×
434
      default:
435
        SCORE_ABORT;
×
436
        return {};
×
437
    }
438
  }
439
  else
440
  {
441
    // Tooltip
442
    if(n.is<Device::AddressSettings>())
×
443
    {
444
      auto& addr_set = n.get<Device::AddressSettings>();
×
445
      if(const auto& desc = ossia::net::get_description(addr_set))
×
446
        return QString::fromStdString(*desc);
×
447
    }
×
448
    else
449
    {
450
      return {};
×
451
    }
452
  }
453
  return {};
×
454
}
91✔
455

456
QVariant
457
DeviceExplorerModel::headerData(int section, Qt::Orientation orientation, int role) const
910✔
458
{
459
  if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
910✔
460
  {
461
    if(section >= 0 && section < (int)Column::Count)
364✔
462
    {
463
      return HEADERS[(Column)section];
364✔
464
    }
465
  }
×
466

467
  return {};
546✔
468
}
910✔
469

470
Qt::ItemFlags DeviceExplorerModel::flags(const QModelIndex& index) const
56✔
471
{
472
  Qt::ItemFlags f = Qt::ItemIsEnabled;
56✔
473
  // by default QAbstractItemModel::flags(index); returns Qt::ItemIsEnabled |
474
  // Qt::ItemIsSelectable
475

476
  if(index.isValid())
56✔
477
  {
478
    const Device::Node& n = nodeFromModelIndex(index);
56✔
479

480
    if(n.isSelectable())
56✔
481
    {
482
      f |= Qt::ItemIsSelectable;
56✔
483
    }
56✔
484

485
    // we allow drag'n drop only from the name column
486
    if(index.column() == (int)Column::Name)
56✔
487
    {
488
      f |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
56✔
489
    }
56✔
490

491
    if(n.isEditable() && index.column() == (int)Column::Value)
56✔
492
    {
493
      f |= Qt::ItemIsEditable;
×
494
    }
×
495

496
    if(index.column() == (int)Column::Name && n.is<Device::AddressSettings>()
56✔
497
       && canRenameNode(n))
56✔
498
    {
499
      f |= Qt::ItemIsEditable;
11✔
500
    }
11✔
501
  }
56✔
502
  else
503
  {
504
    // to be able to drop even where there is nothing
505
    f |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
×
506
  }
507

508
  return f;
56✔
509
}
510

511
/*
512
  return false if no change was made.
513
  dataChanged() & return true if a change is made.
514

515
  Note: this is the function that gets called when the user changes the value
516
  in the tree.
517
  It then sends a command that calls editData.
518
*/
519
bool DeviceExplorerModel::setData(
8✔
520
    const QModelIndex& index, const QVariant& value, int role)
521
{
522
  if(!index.isValid())
8✔
523
    return false;
×
524

525
  auto& n = nodeFromModelIndex(index);
8✔
526

527
  if(!n.is<Device::AddressSettings>())
8✔
528
    return false;
×
529

530
  auto col = Explorer::Column(index.column());
8✔
531

532
  if(role == Qt::EditRole || role == Qt::CheckStateRole)
8✔
533
  {
534
    if(col == Column::Value)
8✔
535
    {
536
      // In this case we don't make a command, but we directly push the
537
      // new value.
538
      auto copy = value.canConvert<ossia::value>() ? value.value<ossia::value>()
5✔
539
                                                   : State::convert::fromQVariant(value);
×
540

541
      // We may have to convert types.
542
      const ossia::value& orig = n.get<Device::AddressSettings>().value;
5✔
543
      if(copy.v.which() != orig.v.which() && !State::convert::convert(orig, copy))
5✔
544
        return false;
×
545

546
      n.get<Device::AddressSettings>().value = copy;
5✔
547

548
      // Note : if we want to disable remote updating, we have to do it
549
      // here (e.g. if this becomes a settings)
550
      m_devicePlugin.updateProxy.updateRemoteValue(Device::address(n).address, copy);
5✔
551

552
      dataChanged(index, index);
5✔
553
      return true;
5✔
554
    }
5✔
555
    else
556
    {
557
      // Here we make a command because we change the structure of the tree.
558
      auto settings = n.get<Device::AddressSettings>();
3✔
559
      if(col == Column::Name)
3✔
560
      {
561
        const QString s = value.toString();
3✔
562
        auto* parent = n.parent();
3✔
563
        if(s.isEmpty() || !parent || !canRenameNode(n))
3✔
564
          return false;
1✔
565

566
        auto renamed = n.get<Device::AddressSettings>();
2✔
567
        renamed.name = s;
2✔
568

569
        if(!checkAddressEditable(*parent, n.get<Device::AddressSettings>(), renamed))
2✔
570
          return false;
1✔
571

572
        settings.name = s;
1✔
573
      }
3✔
574

575
      if(settings != n.get<Device::AddressSettings>())
1✔
576
      {
577
        // We changed
578
        m_cmdQ.redoAndPush(new Explorer::Command::UpdateAddressSettings{
2✔
579
            this->deviceModel(), Device::NodePath{n}, settings});
1✔
580
        return true;
1✔
581
      }
582
    }
3✔
583
  }
×
584

585
  return false;
×
586
}
8✔
587

588
bool DeviceExplorerModel::setHeaderData(int, Qt::Orientation, const QVariant&, int)
×
589
{
590
  return false; // we prevent editing the (column) headers
×
591
}
592

593
/**
594
 * @brief DeviceExplorerModel::editData
595
 *
596
 * This functions gets called by the command
597
 * that edit the columns.
598
 */
599
void DeviceExplorerModel::editData(
×
600
    const Device::NodePath& path, Explorer::Column column, const ossia::value& value,
601
    int role)
602
{
603
  Device::Node* node = path.toNode(&rootNode());
×
604
  editData(*node, column, value, role);
×
605
}
×
606

607
void DeviceExplorerModel::editData(
1✔
608
    Device::Node& node, Column column, const ossia::value& value, int role)
609
{
610
  SCORE_ASSERT(node.parent());
1✔
611

612
  if(node.is<Device::DeviceSettings>())
1✔
613
    return;
×
614

615
  if(role == Qt::EditRole && column == Column::Value)
1✔
616
  {
617
    node.get<Device::AddressSettings>().value = value;
1✔
618
  }
1✔
619

620
  const QModelIndex index = modelIndexFromNode(node, (int)column);
1✔
621
  dataChanged(index, index);
1✔
622
}
1✔
623

624
QModelIndex DeviceExplorerModel::bottomIndex(const QModelIndex& index) const
×
625
{
626
  auto& node = nodeFromModelIndex(index);
×
627

628
  if(!node.hasChildren())
×
629
  {
630
    return index;
×
631
  }
632

633
  return bottomIndex(createIndex(
×
634
      node.childCount() - 1, index.column(), &node.childAt(node.childCount() - 1)));
×
635
}
×
636

637
bool DeviceExplorerModel::isDevice(QModelIndex index) const
×
638
{
639
  if(!index.isValid())
×
640
  {
641
    return false;
×
642
  }
643

644
  const Device::Node& n = nodeFromModelIndex(index);
×
645
  return n.is<Device::DeviceSettings>();
×
646
}
×
647

648
bool DeviceExplorerModel::isEmpty() const
183✔
649
{
650
  return m_rootNode.childCount() == 0;
183✔
651
}
652

653
/*
654
Drag and drop works by deleting the dragged items and creating a new set of
655
dropped items that match those dragged.
656
I will/may call insertRows(), removeRows(), dropMimeData(), ...
657
We define two MimeTypes : address and device.
658
It allows to distinguish whether we are drag'n dropping devices or addresses.
659
 */
660

661
Qt::DropActions DeviceExplorerModel::supportedDropActions() const
×
662
{
663
  return (Qt::CopyAction);
×
664
}
665

666
// Default supportedDragActions() implementation returns
667
// supportedDropActions().
668

669
Qt::DropActions DeviceExplorerModel::supportedDragActions() const
×
670
{
671
  return (Qt::CopyAction);
×
672
}
673

674
QStringList DeviceExplorerModel::mimeTypes() const
×
675
{
676
  return {score::mime::device(), score::mime::address()};
×
677
}
×
678

679
SelectedNodes
680
DeviceExplorerModel::uniqueSelectedNodes(const QModelIndexList& indexes) const
×
681
{
682
  SelectedNodes nodes;
×
683
  ossia::transform(
×
684
      indexes, std::back_inserter(nodes.parents),
×
685
      [&](const QModelIndex& idx) { return &nodeFromModelIndex(idx); });
×
686

687
  ossia::remove_erase(nodes.parents, &m_rootNode);
×
688

689
  nodes.messages.reserve(nodes.messages.size() + nodes.parents.size());
×
690
  if(nodes.parents.size() > 1)
×
691
  {
692
    // Filter messages
693
    for(auto node : nodes.parents)
×
694
    {
695
      if(node->is<Device::AddressSettings>())
×
696
      {
697
        auto& val = node->get<Device::AddressSettings>();
×
698
        if(val.ioType == ossia::access_mode::SET)
×
699
        {
700
          nodes.messages.push_back(node);
×
701
        }
×
702
      }
×
703
    }
704

705
    // Filter parents
706
    nodes.parents = filterUniqueParents(nodes.parents);
×
707
  }
×
708
  else if(nodes.parents.size() == 1)
×
709
  {
710
    Device::Node* node = nodes.parents[0];
×
711
    if(node->is<Device::AddressSettings>() && node->childCount() == 0)
×
712
    {
713
      // If a single node is selected we copy it even if it is "get"
714
      nodes.messages = std::move(nodes.parents);
×
715
    }
×
716
  }
×
717

718
  return nodes;
×
719
}
×
720
// method called when a drag is initiated
721
QMimeData* DeviceExplorerModel::mimeData(const QModelIndexList& indexes) const
×
722
{
723
  auto uniqueNodes = uniqueSelectedNodes(indexes);
×
724

725
  // Handle case of a single device which can have custom mimeData
726
  if(uniqueNodes.parents.size() == 1
×
727
     && uniqueNodes.parents[0]->is<Device::DeviceSettings>())
×
728
  {
729
    auto node = uniqueNodes.parents[0];
×
730
    if(node->is<Device::DeviceSettings>())
×
731
    {
732
      auto& dev = deviceModel().list().device(node->get<Device::DeviceSettings>().name);
×
733
      if(auto d = dev.mimeData())
×
734
        return d;
×
735
    }
×
736
  }
×
737

738
  auto mimeData = new QMimeData;
×
739

740
  // Now we request an update to the device explorer.
741
  m_devicePlugin.updateProxy.refreshRemoteValues(uniqueNodes.parents);
×
742

743
  // The "MessagesList" part.
744
  State::MessageList messages;
×
745
  messages.reserve(uniqueNodes.parents.size() + uniqueNodes.messages.size());
×
746
  for(const auto& node : uniqueNodes.parents)
×
747
  {
748
    Device::parametersList(*node, messages);
×
749
  }
750

751
  for(const auto& node : uniqueNodes.messages)
×
752
  {
753
    messages.push_back(Device::message(*node));
×
754
  }
755

756
  if(!messages.empty())
×
757
  {
758
    Mime<State::MessageList>::Serializer s{*mimeData};
×
759
    s.serialize(messages);
×
760
  }
×
761

762
  // The "Nodes" part. Deserialize with FreeNodeList to get the addresses.
763
  {
764
    Mime<Device::NodeList>::Serializer s{*mimeData};
×
765
    Device::NodeList vec;
×
766
    vec.reserve(uniqueNodes.parents.size() + uniqueNodes.messages.size());
×
767
    vec.insert(vec.end(), uniqueNodes.parents.begin(), uniqueNodes.parents.end());
×
768
    vec.insert(vec.end(), uniqueNodes.messages.begin(), uniqueNodes.messages.end());
×
769
    s.serialize(vec);
×
770
  }
×
771

772
  if(messages.empty() && uniqueNodes.parents.empty() && uniqueNodes.messages.empty())
×
773
  {
774
    delete mimeData;
×
775
    return nullptr;
×
776
  }
777

778
  return mimeData;
×
779
}
×
780

781
bool DeviceExplorerModel::canDropMimeData(
×
782
    const QMimeData* mimeData, Qt::DropAction action, int /*row*/, int /*column*/,
783
    const QModelIndex& parent) const
784
{
785
  if(action == Qt::IgnoreAction)
×
786
  {
787
    return true;
×
788
  }
789

790
  if(action != Qt::MoveAction && action != Qt::CopyAction)
×
791
  {
792
    return false;
×
793
  }
794

795
  if(!mimeData
×
796
     || (!mimeData->hasFormat(score::mime::device())
×
797
         && !mimeData->hasFormat(score::mime::address())))
×
798
  {
799
    return false;
×
800
  }
801

802
  const Device::Node& parentNode = nodeFromModelIndex(parent);
×
803

804
  if(mimeData->hasFormat(score::mime::address()))
×
805
  {
806
    if(&parentNode == &m_rootNode)
×
807
    {
808
      return false;
×
809
    }
810
  }
×
811
  else
812
  {
813
    SCORE_ASSERT(mimeData->hasFormat(score::mime::device()));
×
814

815
    if(&parentNode != &m_rootNode)
×
816
    {
817
      return false;
×
818
    }
819
  }
820

821
  return true;
×
822
}
×
823

824
// method called when a drop occurs
825
// return true if drop really handled, false otherwise.
826
//
827
// if dropMimeData returns true && action==Qt::MoveAction, removeRows is called
828
// immediately after
829
bool DeviceExplorerModel::dropMimeData(
×
830
    const QMimeData* mimeData, Qt::DropAction action, int row, int column,
831
    const QModelIndex& parent)
832
{
833
  if(action == Qt::IgnoreAction)
×
834
  {
835
    return true;
×
836
  }
837

838
  if(action != Qt::MoveAction && action != Qt::CopyAction)
×
839
  {
840
    return false;
×
841
  }
842

843
  if(!mimeData
×
844
     || (!mimeData->hasFormat(score::mime::device())
×
845
         && !mimeData->hasFormat(score::mime::address())))
×
846
  {
847
    return false;
×
848
  }
849

850
  QModelIndex parentIndex; // invalid
×
851
  Device::Node* parentNode = &m_rootNode;
×
852
  QString mimeType = score::mime::device();
×
853

854
  if(mimeData->hasFormat(score::mime::address()))
×
855
  {
856
    parentIndex = parent;
×
857
    parentNode = &nodeFromModelIndex(parent);
×
858
    mimeType = score::mime::address();
×
859

860
    if(parentNode == &m_rootNode)
×
861
    {
862
      return false;
×
863
    }
864
  }
×
865
  else
866
  {
867
    SCORE_ASSERT(mimeData->hasFormat(score::mime::device()));
×
868
    SCORE_ASSERT(mimeType == score::mime::device());
×
869
  }
870

871
  if(parentNode)
×
872
  {
873
    // Note : when dropping a device,
874
    // if there is an existing device that would use the same ports, etc.
875
    // we have to open a dialog to change the device settings.
876

877
    const auto& json = readJson(mimeData->data(mimeType));
×
878
    JSONObject::Deserializer deser{json};
×
879
    Device::Node n;
×
880
    deser.writeTo(n);
×
881

882
    if(mimeType == score::mime::device())
×
883
    {
884
      checkAndLoadDevice(n);
×
885
    }
×
886
    return true;
×
887
  }
×
888
  return false;
×
889
}
×
890

891
void DeviceExplorerModel::checkAndLoadDevice(Device::Node n)
×
892
{
893
  SCORE_ASSERT(n.is<Device::DeviceSettings>());
×
894
  auto& deviceSettings = *n.target<Device::DeviceSettings>();
×
895

896
  // We ask the user to fix the incompatibilities by himself, maybe change
897
  // the name also.
898
  auto dialog = new DeviceEditDialog{
×
899
      *this, m_devicePlugin.context().app.interfaces<Device::ProtocolFactoryList>(),
×
900
      DeviceEditDialog::Creating, QApplication::activeWindow()};
×
901

902
  dialog->setSettings(deviceSettings);
×
903
  dialog->setBrowserEnabled(false);
×
904

905
  connect(
×
906
      dialog, &QDialog::accepted, this, [this, dialog, node = std::move(n)]() mutable {
×
907
        auto new_settings = dialog->getSettings();
×
908
        auto& deviceSettings = *node.target<Device::DeviceSettings>();
×
909
        deviceSettings = new_settings;
×
910
        if(!checkDeviceInstantiatable(deviceSettings))
×
911
        {
912
          score::warning(
×
913
              m_devicePlugin.context().app.mainWindow, "Error",
×
914
              "Could not create the device");
×
915
          dialog->deleteLater();
×
916
          return;
×
917
        }
918
        ossia::net::sanitize_device_name(deviceSettings.name);
×
919

920
        auto cmd = new Command::LoadDevice{deviceModel(), std::move(node)};
×
921
        m_cmdQ.redoAndPush(cmd);
×
922
        dialog->deleteLater();
×
923
      });
×
924
  connect(dialog, &QDialog::rejected, this, [dialog] { dialog->deleteLater(); });
×
925
  dialog->show();
×
926
}
×
927

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

936
  dialog->setSettings(deviceSettings);
×
937
  dialog->setBrowserEnabled(false);
×
938

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

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

960
void DeviceExplorerModel::debug_printPath(const Device::NodePath& path)
×
961
{
962
  const int pathSize = std::ssize(path);
×
963

964
  for(int i = 0; i < pathSize; ++i)
×
965
  {
966
    std::cerr << path.at(i) << " ";
×
967
  }
×
968

969
  std::cerr << "\n";
×
970
}
×
971

972
void DeviceExplorerModel::debug_printIndexes(const QModelIndexList& indexes)
×
973
{
974
  std::cerr << "indexes: " << indexes.size() << " nodes: \n";
×
975
  Q_FOREACH(const QModelIndex& index, indexes)
×
976
  {
977
    if(index.isValid())
×
978
    {
979
      std::cerr << " index.row=" << index.row() << " col=" << index.column() << " ";
×
980
      Device::Node* n = &nodeFromModelIndex(index);
×
981
      std::cerr << " n=" << n << " ";
×
982
      Device::Node* parent = n->parent();
×
983

984
      if(n == &m_rootNode)
×
985
      {
986
        std::cerr << " rootNode parent=" << parent << "\n";
×
987
      }
×
988
      else
989
      {
990
        std::cerr << " n->name=" << n->displayName().toStdString();
×
991
        std::cerr << " parent=" << parent;
×
992
        std::cerr << " parent->name=" << parent->displayName().toStdString() << "\n";
×
993
      }
994
    }
×
995
    else
996
    {
997
      std::cerr << " invalid index \n";
×
998
    }
999
  }
1000
}
×
1001

1002
State::MessageList getSelectionSnapshot(DeviceExplorerModel& model)
×
1003
{
1004
  // Filter
1005
  auto uniqueNodes = model.uniqueSelectedNodes(model.selectedIndexes());
×
1006

1007
  // Recursive refresh
1008
  model.deviceModel().updateProxy.refreshRemoteValues(uniqueNodes.parents);
×
1009

1010
  // Conversion
1011
  State::MessageList messages;
×
1012
  messages.reserve(uniqueNodes.parents.size() + uniqueNodes.messages.size());
×
1013
  for(const auto& node : uniqueNodes.parents)
×
1014
  {
1015
    Device::parametersList(*node, messages);
×
1016
  }
1017
  for(const auto& node : uniqueNodes.messages)
×
1018
  {
1019
    messages.push_back(Device::message(*node));
×
1020
  }
1021

1022
  return messages;
×
1023
}
×
1024

1025
DeviceExplorerModel* try_deviceExplorerFromObject(const QObject& obj)
231✔
1026
{
1027
  auto plug = score::IDocument::documentContext(obj).findPlugin<DeviceDocumentPlugin>();
231✔
1028
  if(plug)
231✔
1029
    return &plug->explorer();
231✔
1030
  return nullptr;
×
1031
}
231✔
1032

1033
DeviceExplorerModel& deviceExplorerFromObject(const QObject& obj)
3✔
1034
{
1035
  auto expl = try_deviceExplorerFromObject(obj);
3✔
1036
  SCORE_ASSERT(expl);
3✔
1037
  return *expl;
3✔
1038
}
1039

1040
DeviceExplorerModel* try_deviceExplorerFromContext(const score::DocumentContext& ctx)
6✔
1041
{
1042
  auto plug = ctx.findPlugin<DeviceDocumentPlugin>();
6✔
1043
  if(plug)
6✔
1044
    return &plug->explorer();
6✔
1045
  return nullptr;
×
1046
}
6✔
1047

1048
DeviceExplorerModel& deviceExplorerFromContext(const score::DocumentContext& ctx)
6✔
1049
{
1050
  auto expl = try_deviceExplorerFromContext(ctx);
6✔
1051
  SCORE_ASSERT(expl);
6✔
1052
  return *expl;
6✔
1053
}
1054

1055
Device::FullAddressAccessorSettings makeFullAddressAccessorSettings(
×
1056
    const State::AddressAccessor& addr, const score::DocumentContext& ctx,
1057
    ossia::value min, ossia::value max, ossia::value val)
1058
{
1059
  // First try to find if there is a matching address
1060
  // in the device explorer
1061
  auto deviceexplorer = Explorer::try_deviceExplorerFromContext(ctx);
×
1062
  if(deviceexplorer)
×
1063
  {
1064
    return Device::makeFullAddressAccessorSettings(
×
1065
        addr, *deviceexplorer, std::move(min), std::move(max), std::move(val));
×
1066
  }
1067

1068
  // If there is none, build with some default settings
1069
  Device::FullAddressAccessorSettings s;
×
1070
  s.address = addr;
×
1071
  s.value = std::move(val);
×
1072
  s.domain = ossia::make_domain(std::move(min), std::move(max));
×
1073
  return s;
×
1074
}
×
1075
}
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