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

ossia / score / 35310244323

18 Sep 2026 05:17AM UTC coverage: 39.902% (+2.2%) from 37.724%
35310244323

push

github

jcelerier
tests: include CommandDispatcher where JsLibraryScriptSavingTest uses it

ee4b67af6f added the test with a CommandDispatcher<> but without its header,
relying on it arriving transitively. The Coverage build compiles with clang-19
at gnu++23 and does not get it that way:

  JsLibraryScriptSavingTest.cpp:70:3: error: no template named 'CommandDispatcher'

This has been broken since 2026-09-13 and was hidden behind the boost download
failure, which stopped that job before it ever reached the compile.

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

114906 of 287968 relevant lines covered (39.9%)

77906.02 hits per line

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

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

5
#include "DeviceExplorerFilterProxyModel.hpp"
6
#include "DeviceExplorerView.hpp"
7
#include "ExplorationWorkerWrapper.hpp"
8
#include "QProgressIndicator.h"
9
#include "Widgets/AddressEditDialog.hpp"
10
#include "Widgets/DeviceEditDialog.hpp"
11

12
#include <State/Address.hpp>
13
#include <State/Value.hpp>
14

15
#include <Device/Address/AddressSettings.hpp>
16
#include <Device/Loading/JamomaDeviceLoader.hpp>
17
#include <Device/Loading/ScoreDeviceLoader.hpp>
18
#include <Device/Protocol/DeviceInterface.hpp>
19
#include <Device/Protocol/DeviceSettings.hpp>
20
#include <Device/Protocol/ProtocolList.hpp>
21

22
#include <Explorer/Commands/Add/AddAddress.hpp>
23
#include <Explorer/Commands/Add/LoadDevice.hpp>
24
#include <Explorer/Explorer/LearnRollback.hpp>
25
#include <Explorer/Commands/Remove.hpp>
26
#include <Explorer/Commands/RemoveNodes.hpp>
27
#include <Explorer/Commands/ReplaceDevice.hpp>
28
#include <Explorer/Commands/Update/UpdateAddressSettings.hpp>
29
#include <Explorer/Commands/Update/UpdateDeviceSettings.hpp>
30
#include <Explorer/DeviceList.hpp>
31
#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
32
#include <Explorer/Explorer/AddressItemModel.hpp>
33
#include <Explorer/Explorer/DeviceExplorerDelegate.hpp>
34
#include <Explorer/Explorer/DeviceExplorerModel.hpp>
35
#include <Explorer/Listening/ListeningHandler.hpp>
36
#include <Explorer/Panel/DeviceExplorerPanelDelegate.hpp>
37

38
#include <score/command/Dispatchers/CommandDispatcher.hpp>
39
#include <score/command/Dispatchers/MacroCommandDispatcher.hpp>
40
#include <score/model/IdentifiedObject.hpp>
41
#include <score/model/path/Path.hpp>
42
#include <score/model/tree/InvisibleRootNode.hpp>
43
#include <score/model/tree/TreeNode.hpp>
44
#include <score/model/tree/TreeNodeSerialization.hpp>
45
#include <score/model/tree/TreeViewExpandState.hpp>
46
#include <score/plugins/application/GUIApplicationPlugin.hpp>
47
#include <score/serialization/AnySerialization.hpp>
48
#include <score/serialization/MapSerialization.hpp>
49
#include <score/tools/Bind.hpp>
50
#include <score/tools/std/Optional.hpp>
51
#include <score/widgets/HelpInteraction.hpp>
52
#include <score/widgets/SearchLineEdit.hpp>
53
#include <score/widgets/SetIcons.hpp>
54
#include <score/widgets/SignalUtils.hpp>
55

56
#include <ossia/detail/algorithms.hpp>
57
#include <ossia/detail/ssize.hpp>
58

59
#include <ossia-qt/js_utilities.hpp>
60

61
#include <score/tools/FilePath.hpp>
62

63
#include <QAbstractProxyModel>
64
#include <QAction>
65
#include <QClipboard>
66
#include <QComboBox>
67
#include <QContextMenuEvent>
68
#include <QDialog>
69
#include <QDialogButtonBox>
70
#include <QFileDialog>
71
#include <QGridLayout>
72
#include <QHeaderView>
73
#include <QIcon>
74
#include <QKeySequence>
75
#include <QLabel>
76
#include <QLineEdit>
77
#include <QList>
78
#include <QListWidget>
79
#include <QMenu>
80
#include <QPair>
81
#include <QRegularExpression>
82
#include <QShortcut>
83
#include <QSize>
84
#include <QStackedLayout>
85
#include <QString>
86
#include <QStringList>
87
#include <QTableView>
88
#include <QTimer>
89
#include <QToolButton>
90
#include <QTreeView>
91
#include <qnamespace.h>
92

93
#include <wobjectimpl.h>
94

95
#include <functional>
96
#include <set>
97
#include <stdexcept>
98
W_OBJECT_IMPL(Explorer::DeviceExplorerWidget)
42,604✔
99
namespace Explorer
100
{
101
static const Device::DeviceSettings* getDevice(const Device::Node& n)
×
102
{
103
  if(n.is<Device::AddressSettings>())
×
104
  {
105
    const Device::Node* p = &n;
×
106
    while((p = p->parent()))
×
107
    {
108
      if(p->is<Device::DeviceSettings>())
×
109
        return &p->get<Device::DeviceSettings>();
×
110
    }
111
  }
×
112
  else if(n.is<Device::DeviceSettings>())
×
113
  {
114
    return &n.get<Device::DeviceSettings>();
×
115
  }
116
  return nullptr;
×
117
}
×
118

119
class ExplorerSearchLineEdit final : public score::SearchLineEdit
120
{
121
  using ExpandState = score::TreeViewExpandState<Device::Node, Device::NodePath>;
122
  ExpandState m_expandState;
123

124
public:
125
  ExplorerSearchLineEdit(DeviceExplorerWidget& parent)
2,156✔
126
      : score::SearchLineEdit{&parent}
1,078✔
127
      , m_widget{parent}
1,078✔
128
  {
1,078✔
129
    connect(this, &QLineEdit::textEdited, this, [this] { search(); });
1,078✔
130
  }
1,078✔
131

132
  void search() override
316✔
133
  {
134
    auto m = m_widget.proxyModel();
316✔
135
    if(!m)
316✔
136
      return;
×
137
    auto v = m_widget.view();
316✔
138
    if(!v)
316✔
139
      return;
×
140

141
    const QString& current_filter = m->filterRegularExpression().pattern();
316✔
142
    const QString& new_filter = text();
316✔
143

144
    if(current_filter.isEmpty())
316✔
145
    {
146
      m_expandState.save(m, v);
316✔
147
    }
316✔
148

149
    if(new_filter != current_filter)
316✔
150
    {
151
      m->setFilterFixedString(new_filter);
×
152
      m->setFilterCaseSensitivity(Qt::CaseInsensitive);
×
153
      if(new_filter.isEmpty())
×
154
      {
155
        // Restore the indices
156
        m_expandState.restore(m, v);
×
157
      }
×
158
      else
159
      {
160
        v->expandAll();
×
161
      }
162
    }
×
163
  }
316✔
164

165
  DeviceExplorerWidget& m_widget;
166
};
167

168
class LearnDialog final : public QDialog
169
{
170
public:
171
  LearnDialog(Device::DeviceInterface& dev, QWidget* w)
×
172
      : QDialog{w}
×
173
      , m_dev{dev}
×
174
  {
×
175
    this->setWindowTitle(dev.name() + tr(": learning"));
×
176
    auto lay = new QVBoxLayout{this};
×
177

178
    QDialogButtonBox* buttonBox = new QDialogButtonBox(
×
179
        QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, this);
×
180
    connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
×
181
    connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
182

183
    m_list = new QListWidget{this};
×
184
    lay->addWidget(m_list);
×
185
    lay->addWidget(buttonBox);
×
186

187
    con(dev, &Device::DeviceInterface::pathAdded, this, [this](const State::Address& a) {
×
188
      m_list->addItem(a.toString());
×
189
      m_list->scrollToBottom();
×
190
    });
×
191

192
    m_dev.setLearning(true);
×
193
  }
×
194

195
  ~LearnDialog() { m_dev.setLearning(false); }
×
196

197
  Device::DeviceInterface& m_dev;
198
  QListWidget* m_list{};
×
199
};
200

201
DeviceExplorerWidget::DeviceExplorerWidget(
1,078✔
202
    const Device::ProtocolFactoryList& pl, QWidget* parent)
203
    : QWidget(parent)
1,078✔
204
    , m_protocolList{pl}
1,078✔
205
    , m_proxyModel(nullptr)
206
    , m_deviceDialog(nullptr)
207
{
1,078✔
208
  setMinimumWidth(150);
1,078✔
209
  buildGUI();
1,078✔
210

211
  // Set the expansion signals
212
  connect(
1,078✔
213
      m_ntView, &DeviceExplorerView::created, this,
1,078✔
214
      [&](const QModelIndex& parent, int start, int end) {
1,139✔
215
    // Only what is visible is listened to: rows under a folded parent will
216
    // be when it is unfolded (QTreeView::expanded below).
217
    if(m_listeningManager && parent.isValid() && m_ntView->isExpanded(parent))
61✔
218
    {
219
      for(int i = start; i <= end; i++)
5✔
220
      {
221
        Device::Node* node{};
3✔
222
        if(m_ntView->hasProxy())
3✔
223
        {
224
          node = (Device::Node*)sourceIndex(
6✔
225
                     ((QTreeView*)m_ntView)->model()->index(i, 0, parent))
3✔
226
                     .internalPointer();
3✔
227
        }
3✔
228
        else
229
        {
230
          node = ((Device::Node*)model()->index(i, 0, parent).internalPointer());
×
231
        }
232

233
        m_listeningManager->enableListening(*node);
3✔
234
      }
3✔
235
    }
2✔
236
      });
61✔
237
  connect(m_ntView, &QTreeView::expanded, this, [&](const QModelIndex& idx) {
1,081✔
238
    if(m_listeningManager)
3✔
239
      m_listeningManager->setListening(idx, true);
3✔
240
  });
3✔
241
  connect(m_ntView, &QTreeView::collapsed, this, [&](const QModelIndex& idx) {
1,080✔
242
    if(m_listeningManager)
2✔
243
      m_listeningManager->setListening(idx, false);
2✔
244
  });
2✔
245
}
×
246

247
void DeviceExplorerWidget::buildGUI()
1,078✔
248
{
249
  m_ntView = new DeviceExplorerView(this);
1,078✔
250
  m_ntView->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
1,078✔
251
  m_ntView->setItemDelegate(new DeviceExplorerDelegate{m_ntView});
1,078✔
252

253
  m_addressModel = new AddressItemModel{this};
1,078✔
254
  m_addressView = new QTableView{this};
1,078✔
255
  {
256
    auto qs = new QShortcut(QKeySequence::Copy, m_addressView, m_addressView, [this]() {
1,078✔
257
      auto idx = m_addressView->currentIndex();
×
258
      auto data = m_addressView->model()->data(idx);
×
259
      qApp->clipboard()->setText(data.toString());
×
260
    }, [] {});
×
261
    qs->setContext(Qt::WidgetShortcut);
1,078✔
262
  }
263
  auto delegate = new AddressItemDelegate{m_addressView};
1,078✔
264
  m_addressView->setItemDelegate(delegate);
1,078✔
265
  m_addressView->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
1,078✔
266
  m_addressView->setMinimumHeight(100);
1,078✔
267

268
  m_addressView->horizontalHeader()->hide();
1,078✔
269
  m_addressView->verticalHeader()->hide();
1,078✔
270
  m_addressView->horizontalHeader()->setCascadingSectionResizes(true);
1,078✔
271
  m_addressView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
1,078✔
272
  m_addressView->horizontalHeader()->setStretchLastSection(true);
1,078✔
273
  m_addressView->horizontalHeader()->setResizeContentsPrecision(-1);
1,078✔
274
  m_addressView->setAlternatingRowColors(true);
1,078✔
275
  m_addressView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
1,078✔
276
  m_addressView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
1,078✔
277
  m_addressView->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
1,078✔
278
  m_addressView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
1,078✔
279
  m_addressView->verticalHeader()->setDefaultSectionSize(14);
1,078✔
280
  m_addressView->verticalHeader()->setResizeContentsPrecision(-1);
1,078✔
281
  connect(
1,078✔
282
      m_addressView->horizontalHeader(), &QHeaderView::sectionResized, m_addressView,
1,078✔
283
      &QTableView::resizeRowsToContents);
284

285
  m_addressView->setModel(m_addressModel);
1,078✔
286
  connect(
1,078✔
287
      m_ntView,
1,078✔
288
      static_cast<void (DeviceExplorerView::*)()>(&DeviceExplorerView::selectionChanged),
289
      this,
290
      [this] {
2,730✔
291
    updateAddressView();
1,652✔
292
    updateActions();
1,652✔
293
      },
1,652✔
294
      Qt::QueuedConnection);
295

296
  m_editAction = new QAction(tr("Edit"), this);
1,078✔
297
  score::setHelp(m_editAction, tr("Edit the device."));
1,078✔
298

299
  m_refreshAction = new QAction(tr("Refresh namespace"), this);
1,078✔
300
  m_refreshAction->setShortcut(QKeySequence::Refresh);
1,078✔
301

302
  m_disconnect = new QAction{tr("Disconnect"), this};
1,078✔
303
  m_reconnect = new QAction{tr("Reconnect"), this};
1,078✔
304

305
  m_refreshValueAction = new QAction(tr("Refresh value"), this);
1,078✔
306

307
  m_removeNodeAction = new QAction(tr("Remove"), this);
1,078✔
308
  m_importDeviceAction = new QAction{tr("Import device"), this};
1,078✔
309
  m_exportDeviceAction = new QAction{tr("Export device"), this};
1,078✔
310
  m_learnAction = new QAction{tr("Learn"), this};
1,078✔
311
  m_findUsageAction = new QAction{tr("Find usage"), this};
1,078✔
312

313
#ifdef __APPLE__
314
  m_removeNodeAction->setShortcut(QKeySequence(tr("Ctrl+Backspace")));
315
#else
316
  m_removeNodeAction->setShortcut(QKeySequence::Delete);
1,078✔
317
#endif
318

319
  m_editAction->setEnabled(false);
1,078✔
320
  m_refreshAction->setEnabled(false);
1,078✔
321
  m_refreshValueAction->setEnabled(false);
1,078✔
322
  m_removeNodeAction->setEnabled(false);
1,078✔
323
  m_disconnect->setEnabled(false);
1,078✔
324
  m_reconnect->setEnabled(false);
1,078✔
325
  m_importDeviceAction->setEnabled(true);
1,078✔
326
  m_exportDeviceAction->setEnabled(false);
1,078✔
327
  m_learnAction->setEnabled(false);
1,078✔
328
  m_findUsageAction->setEnabled(false);
1,078✔
329

330
  m_editAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
331
  m_refreshAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
332
  m_refreshValueAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
333
  m_removeNodeAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
334
  m_disconnect->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
335
  m_reconnect->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
336
  m_importDeviceAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
337
  m_exportDeviceAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
338
  m_learnAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
339
  m_findUsageAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
1,078✔
340

341
  connect(m_editAction, &QAction::triggered, this, &DeviceExplorerWidget::edit);
1,078✔
342
  connect(m_refreshAction, &QAction::triggered, this, &DeviceExplorerWidget::refresh);
1,078✔
343
  connect(
1,078✔
344
      m_refreshValueAction, &QAction::triggered, this,
1,078✔
345
      &DeviceExplorerWidget::refreshValue);
346
  connect(m_disconnect, &QAction::triggered, this, &DeviceExplorerWidget::disconnect);
1,078✔
347
  connect(m_reconnect, &QAction::triggered, this, &DeviceExplorerWidget::reconnect);
1,078✔
348
  connect(
1,078✔
349
      m_removeNodeAction, &QAction::triggered, this, &DeviceExplorerWidget::removeNodes);
1,078✔
350
  connect(
1,078✔
351
      m_importDeviceAction, &QAction::triggered, this,
1,078✔
352
      &DeviceExplorerWidget::importDevice);
353
  connect(
1,078✔
354
      m_exportDeviceAction, &QAction::triggered, this,
1,078✔
355
      &DeviceExplorerWidget::exportDevice);
356
  connect(m_learnAction, &QAction::triggered, this, &DeviceExplorerWidget::learn);
1,078✔
357
  connect(
1,078✔
358
      m_findUsageAction, &QAction::triggered, this, &DeviceExplorerWidget::findUsage);
1,078✔
359

360
  m_openMenu = new QToolButton(this);
1,078✔
361
  m_openMenu->setIcon(makeIcons(
2,156✔
362
      QStringLiteral(":/icons/add_on.png"), QStringLiteral(":/icons/add_off.png"),
1,078✔
363
      QStringLiteral(":/icons/add_disabled.png")));
1,078✔
364
  m_openMenu->setAutoRaise(true);
1,078✔
365

366
  m_addDeviceAction = new QAction(tr("Add device"), this);
1,078✔
367
  setIcons(
1,078✔
368
      m_addDeviceAction, QStringLiteral(":/icons/add_device_on.png"),
1,078✔
369
      QStringLiteral(":/icons/add_device_off.png"),
1,078✔
370
      QStringLiteral(":/icons/add_device_disabled.png"));
1,078✔
371
  m_addDeviceAction->setShortcut(tr("Ctrl+B"));
1,078✔
372

373
  m_addSiblingAction = new QAction(tr("Add sibling"), this);
1,078✔
374
  setIcons(
1,078✔
375
      m_addSiblingAction, QStringLiteral(":/icons/add_sibling_on.png"),
1,078✔
376
      QStringLiteral(":/icons/add_sibling_off.png"),
1,078✔
377
      QStringLiteral(":/icons/add_sibling_disabled.png"));
1,078✔
378

379
  m_addChildAction = new QAction(tr("Add child"), this);
1,078✔
380
  setIcons(
1,078✔
381
      m_addChildAction, QStringLiteral(":/icons/add_child_on.png"),
1,078✔
382
      QStringLiteral(":/icons/add_child_off.png"),
1,078✔
383
      QStringLiteral(":/icons/add_child_disabled.png"));
1,078✔
384
  connect(
1,078✔
385
      m_addDeviceAction, &QAction::triggered, this, &DeviceExplorerWidget::addDevice);
1,078✔
386
  connect(
1,078✔
387
      m_addSiblingAction, &QAction::triggered, this, &DeviceExplorerWidget::addSibling);
1,078✔
388
  connect(m_addChildAction, &QAction::triggered, this, &DeviceExplorerWidget::addChild);
1,078✔
389

390
  m_addSiblingAction->setEnabled(false);
1,078✔
391
  m_addChildAction->setEnabled(false);
1,078✔
392

393
  // Setup menus
394

395
  QMenu* addMenu = new QMenu(this);
1,078✔
396
  addMenu->addAction(m_addDeviceAction);
1,078✔
397
  addMenu->addAction(m_importDeviceAction);
1,078✔
398
  addMenu->addAction(m_addSiblingAction);
1,078✔
399
  addMenu->addAction(m_addChildAction);
1,078✔
400
  addMenu->addAction(m_exportDeviceAction);
1,078✔
401
  addMenu->addSeparator();
1,078✔
402
  addMenu->addAction(m_removeNodeAction);
1,078✔
403

404
  connect(m_openMenu, &QToolButton::clicked, addMenu, [addMenu]() {
1,078✔
405
    addMenu->popup(QCursor::pos());
×
406
  });
×
407

408
  // Add actions to the current widget so that shortcuts work
409
  {
410
    this->addAction(m_addDeviceAction);
1,078✔
411
    this->addAction(m_importDeviceAction);
1,078✔
412
    this->addAction(m_addSiblingAction);
1,078✔
413
    this->addAction(m_addChildAction);
1,078✔
414
    this->addAction(m_exportDeviceAction);
1,078✔
415

416
    this->addAction(m_refreshAction);
1,078✔
417
    this->addAction(m_refreshValueAction);
1,078✔
418
    this->addAction(m_learnAction);
1,078✔
419
    this->addAction(m_findUsageAction);
1,078✔
420

421
    this->addAction(m_removeNodeAction);
1,078✔
422
  }
423

424
  m_columnCBox = new QComboBox(this);
1,078✔
425
  m_nameLEdit = new ExplorerSearchLineEdit(*this);
1,078✔
426

427
  connect(
1,078✔
428
      m_columnCBox, SignalUtils::QComboBox_currentIndexChanged_int(), m_nameLEdit,
1,078✔
429
      [this] {
1,394✔
430
    m_proxyModel->setColumn((Explorer::Column)m_columnCBox->currentIndex());
316✔
431
    m_nameLEdit->search();
316✔
432
      });
316✔
433

434
  auto hLayout = new score::MarginLess<QHBoxLayout>;
1,078✔
435
  hLayout->setSpacing(0);
1,078✔
436
  hLayout->addWidget(m_openMenu);
1,078✔
437
  hLayout->addWidget(m_columnCBox);
1,078✔
438
  hLayout->addWidget(m_nameLEdit);
1,078✔
439

440
  QWidget* mainWidg = new QWidget;
1,078✔
441
  mainWidg->setContentsMargins(0, 0, 0, 2);
1,078✔
442
  auto vLayout = new score::MarginLess<QVBoxLayout>;
1,078✔
443
  vLayout->addLayout(hLayout);
1,078✔
444
  vLayout->addWidget(m_ntView);
1,078✔
445
  vLayout->addWidget(m_addressView);
1,078✔
446
  mainWidg->setLayout(vLayout);
1,078✔
447
  mainWidg->setObjectName("DeviceExplorer");
1,078✔
448

449
  m_lay = new QStackedLayout;
1,078✔
450
  m_lay->addWidget(mainWidg);
1,078✔
451

452
  auto refreshParent = new QWidget;
1,078✔
453
  auto refreshLay = new QGridLayout;
1,078✔
454
  refreshParent->setLayout(refreshLay);
1,078✔
455
  m_refreshIndicator = new QProgressIndicator{refreshParent};
1,078✔
456
  QPalette palette;
1,078✔
457
  palette.setBrush(QPalette::Window, Qt::transparent);
1,078✔
458
  m_refreshIndicator->setPalette(palette);
1,078✔
459

460
  refreshLay->addWidget(m_refreshIndicator);
1,078✔
461
  m_lay->addWidget(refreshParent);
1,078✔
462
  setLayout(m_lay);
1,078✔
463
}
1,078✔
464

465
void DeviceExplorerWidget::blockGUI(bool b)
×
466
{
467
  m_ntView->setDisabled(b);
×
468
  m_addressView->setDisabled(b);
×
469
  if(b)
×
470
  {
471
    // m_ntView to front
472
    m_lay->setCurrentIndex(1);
×
473
    m_refreshIndicator->startAnimation();
×
474
  }
×
475
  else
476
  {
477
    // progress widget to front
478
    m_lay->setCurrentIndex(0);
×
479
    m_refreshIndicator->stopAnimation();
×
480
  }
481
}
×
482

483
void DeviceExplorerWidget::setEditable(bool b)
590✔
484
{
485
  if(m_openMenu)
590✔
486
    m_openMenu->setEnabled(b);
590✔
487
  m_addressView->setEnabled(b);
590✔
488
}
590✔
489

490
QModelIndex DeviceExplorerWidget::sourceIndex(QModelIndex index) const
13✔
491
{
492
  if(m_ntView->hasProxy())
13✔
493
    index = static_cast<const QAbstractProxyModel*>(m_ntView->QTreeView::model())
26✔
494
                ->mapToSource(index);
13✔
495
  return index;
13✔
496
}
497

498
QModelIndex DeviceExplorerWidget::proxyIndex(QModelIndex index) const
96✔
499
{
500
  if(m_ntView->hasProxy())
96✔
501
    index = static_cast<const QAbstractProxyModel*>(m_ntView->QTreeView::model())
192✔
502
                ->mapFromSource(index);
96✔
503
  return index;
96✔
504
}
505

506
QSize DeviceExplorerWidget::sizeHint() const
20✔
507
{
508
  return {200, 800};
20✔
509
}
510

511
void DeviceExplorerWidget::contextMenuEvent(QContextMenuEvent* event)
×
512
{
513
  updateActions();
×
514
  QMenu* contextMenu = new QMenu{this};
×
515

516
  if(auto m = model())
×
517
  {
518
    if(!m->isEmpty())
×
519
    {
520
      QModelIndexList selection = m_ntView->selectedIndexes();
×
521

522
      if(selection.size() == 1)
×
523
      {
524
        auto& node = m->nodeFromModelIndex(m_ntView->selectedIndex());
×
525
        if(node.is<Device::DeviceSettings>())
×
526
        {
527
          auto& lst = m->deviceModel().list();
×
528
          auto& dev = lst.device(node.get<Device::DeviceSettings>().name);
×
529
          dev.setupContextMenu(*contextMenu);
×
530
          contextMenu->addSeparator();
×
531
        }
×
532
      }
×
533
    }
×
534
  }
×
535

536
  contextMenu->addAction(m_editAction);
×
537
  contextMenu->addAction(m_refreshAction);
×
538
  contextMenu->addAction(m_refreshValueAction);
×
539

540
  contextMenu->addAction(m_disconnect);
×
541
  contextMenu->addAction(m_reconnect);
×
542

543
  contextMenu->addSeparator();
×
544
  contextMenu->addAction(m_addDeviceAction);
×
545
  contextMenu->addAction(m_importDeviceAction);
×
546
  contextMenu->addAction(m_addSiblingAction);
×
547
  contextMenu->addAction(m_addChildAction);
×
548
  contextMenu->addAction(m_exportDeviceAction);
×
549
  contextMenu->addAction(m_learnAction);
×
550
  contextMenu->addAction(m_findUsageAction);
×
551
  contextMenu->addSeparator();
×
552
  contextMenu->addAction(m_removeNodeAction);
×
553

554
  contextMenu->exec(event->globalPos());
×
555
  contextMenu->deleteLater();
×
556
}
×
557

558
void DeviceExplorerWidget::setModel(DeviceExplorerModel* model)
1,653✔
559
{
560
  delete m_proxyModel; //? will also delete previous model ??
1,653✔
561
  m_proxyModel = nullptr;
1,653✔
562
  m_listeningManager.reset();
1,653✔
563
  QObject::disconnect(m_modelCon);
1,653✔
564
  QObject::disconnect(m_addressCon);
1,653✔
565
  for(auto& con : m_listeningCons)
2,541✔
566
    QObject::disconnect(con);
888✔
567
  m_listeningCons.clear();
1,653✔
568
  m_savedExpansion.clear();
1,653✔
569

570
  if(model)
1,653✔
571
  {
572
    m_proxyModel = new DeviceExplorerFilterProxyModel(this);
297✔
573
    m_proxyModel->setSourceModel(model);
297✔
574
    m_ntView->setModel(m_proxyModel);
297✔
575
    model->setView(m_ntView);
297✔
576

577
    m_listeningManager = std::make_unique<ListeningManager>(*model, *this);
297✔
578
    m_cmdDispatcher = std::make_unique<CommandDispatcher<>>(model->commandStack());
297✔
579

580
    populateColumnCBox();
297✔
581

582
    updateActions();
297✔
583

584
    m_modelCon = connect(
297✔
585
        model, &DeviceExplorerModel::nodeChanged, this, [this](Device::Node* n) {
310✔
586
          bool parent_is_expanded = m_ntView->isExpanded(
26✔
587
              proxyIndex(m_ntView->model()->modelIndexFromNode(*n->parent(), 0)));
13✔
588
          if(parent_is_expanded)
13✔
589
          {
590
            if(m_listeningManager)
×
591
              m_listeningManager->enableListening(*n);
×
592
          }
×
593
        });
13✔
594

595
    // What leaves the tree is no longer listened to - whatever the reason
596
    // (removal of a node or of a device, a device tree being swapped). The
597
    // device forgets the request so that a later restoreListening() does not
598
    // bring nodes back that are not shown.
599
    m_listeningCons.push_back(connect(
594✔
600
        model, &QAbstractItemModel::rowsAboutToBeRemoved, this,
297✔
601
        [this, model](const QModelIndex& parent, int first, int last) {
303✔
602
      if(!m_listeningManager)
6✔
603
        return;
×
604
      for(int i = first; i <= last; i++)
15✔
605
      {
606
        auto idx = model->index(i, 0, parent);
9✔
607
        if(idx.isValid())
9✔
608
          m_listeningManager->disableListening(model->nodeFromModelIndex(idx));
9✔
609
      }
9✔
610
        }));
6✔
611

612
    // A device tree being swapped in place (after re-exploring its namespace)
613
    // keeps the unfolding it had: the same addresses end up shown, and
614
    // expanding them again is what brings the listening back.
615
    m_listeningCons.push_back(connect(
594✔
616
        model, &DeviceExplorerModel::deviceTreeAboutToChange, this,
297✔
617
        [this, model](Device::Node* device) {
300✔
618
      m_savedExpansion.clear();
3✔
619
      if(!device)
3✔
620
        return;
×
621
      auto dev_idx = model->modelIndexFromNode(*device, 0);
3✔
622
      if(!m_ntView->isExpanded(proxyIndex(dev_idx)))
3✔
623
        return;
1✔
624

625
      // Pre-order, so that parents are expanded before their children
626
      std::function<void(const QModelIndex&, const QStringList&)> visit
627
          = [&](const QModelIndex& parent, const QStringList& path) {
4✔
628
        const int n = model->rowCount(parent);
2✔
629
        for(int i = 0; i < n; i++)
6✔
630
        {
631
          auto idx = model->index(i, 0, parent);
4✔
632
          if(!m_ntView->isExpanded(proxyIndex(idx)))
4✔
633
            continue;
4✔
634
          auto& node = model->nodeFromModelIndex(idx);
×
635
          QStringList sub = path;
×
636
          sub.push_back(node.displayName());
×
637
          m_savedExpansion.push_back(sub);
×
638
          visit(idx, sub);
×
639
        }
×
640
      };
2✔
641
      visit(dev_idx, {});
2✔
642
        }));
3✔
643

644
    m_listeningCons.push_back(connect(
594✔
645
        model, &DeviceExplorerModel::deviceTreeChanged, this,
297✔
646
        [this, model](Device::Node* device) {
300✔
647
      auto saved = std::move(m_savedExpansion);
3✔
648
      m_savedExpansion.clear();
3✔
649
      if(!device || saved.empty())
3✔
650
        return;
3✔
651

652
      for(const auto& path : saved)
×
653
      {
654
        // Walk down by name; a path that no longer exists is simply skipped
655
        Device::Node* node = device;
×
656
        for(const auto& name : path)
×
657
        {
658
          auto it = ossia::find_if(
×
659
              *node, [&](const Device::Node& c) { return c.displayName() == name; });
×
660
          if(it == node->end())
×
661
          {
662
            node = nullptr;
×
663
            break;
×
664
          }
665
          node = &*it;
×
666
        }
667
        if(node)
×
668
          m_ntView->setExpanded(proxyIndex(model->modelIndexFromNode(*node, 0)), true);
×
669
      }
670
        }));
3✔
671

672
    connect(
297✔
673
        model, &DeviceExplorerModel::dataChanged, this,
297✔
674
        [this](
354✔
675
            const QModelIndex& topLeft, const QModelIndex& bottomRight,
676
            const QVector<int>& roles) {
677
      auto indexes = m_ntView->selectedIndexes();
57✔
678

679
      if(indexes.size() == 1)
57✔
680
      {
681
        auto selected = sourceIndex(indexes.first());
×
682

683
        if(selected.parent() == topLeft.parent() && selected.row() == topLeft.row())
×
684
          updateAddressView();
×
685
      }
×
686
        });
57✔
687
  }
297✔
688
  else
689
  {
690
    m_ntView->setModel((QAbstractItemModel*)nullptr);
1,356✔
691
  }
692

693
  setEnabled(bool(model));
1,653✔
694
}
1,653✔
695

696
void DeviceExplorerWidget::populateColumnCBox()
297✔
697
{
698
  SCORE_ASSERT(model());
297✔
699
  SCORE_ASSERT(m_columnCBox);
297✔
700

701
  QStringList columns = model()->getColumns();
297✔
702
  m_columnCBox->clear();
297✔
703
  m_columnCBox->addItems(columns);
297✔
704
}
297✔
705

706
// The bool indicates if the passed node was a device
707
std::pair<Device::DeviceCapas, bool>
708
getCapas(Device::Node* p, const Device::DeviceList& lst)
×
709
{
710
  if(p->is<Device::DeviceSettings>())
×
711
  {
712
    return {lst.device(p->get<Device::DeviceSettings>().name).capabilities(), true};
×
713
  }
714
  while(p && !p->is<Device::DeviceSettings>())
×
715
  {
716
    p = p->parent();
×
717
  }
718
  if(!p)
×
719
    throw std::runtime_error("Cannot get capabilities of no device");
×
720

721
  return {lst.device(p->get<Device::DeviceSettings>().name).capabilities(), false};
×
722
}
×
723

724
void DeviceExplorerWidget::updateActions()
1,949✔
725
{
726
  auto m = model();
1,949✔
727
  if(!m)
1,949✔
728
    return;
1,354✔
729

730
  const bool editable = this->editable();
595✔
731
  m_addDeviceAction->setEnabled(true);
595✔
732
  m_importDeviceAction->setEnabled(true);
595✔
733
  m_exportDeviceAction->setEnabled(false);
595✔
734
  m_learnAction->setEnabled(false);
595✔
735
  m_addSiblingAction->setEnabled(false);
595✔
736
  m_addChildAction->setEnabled(false);
595✔
737
  m_editAction->setEnabled(false);
595✔
738
  m_refreshAction->setEnabled(false);
595✔
739
  m_refreshValueAction->setEnabled(false);
595✔
740
  m_removeNodeAction->setEnabled(false);
595✔
741
  m_findUsageAction->setEnabled(false);
595✔
742
  m_reconnect->setEnabled(false);
595✔
743
  m_disconnect->setEnabled(false);
595✔
744

745
  if(!m->isEmpty())
595✔
746
  {
747
    // TODO: choice for multi selection
748

749
    SCORE_ASSERT(m_ntView);
×
750

751
    QModelIndexList selection = m_ntView->selectedIndexes();
×
752
    if(!selection.isEmpty())
×
753
    {
754
      m_findUsageAction->setEnabled(true);
×
755
    }
×
756

757
    if(selection.size() == 1)
×
758
    {
759
      const auto [capas, aDeviceIsSelected] = getCapas(
×
760
          &m->nodeFromModelIndex(m_ntView->selectedIndex()), m->deviceModel().list());
×
761

762
      if(!aDeviceIsSelected)
×
763
      {
764
        m_refreshValueAction->setEnabled(capas.canRefreshValue && editable);
×
765
        m_addSiblingAction->setEnabled(capas.canAddNode && editable);
×
766
        m_addChildAction->setEnabled(capas.canAddNode && editable);
×
767
        m_removeNodeAction->setEnabled(capas.canRemoveNode && editable);
×
768
      }
×
769
      else
770
      {
771
        m_refreshAction->setEnabled(capas.canRefreshTree && editable);
×
772
        m_reconnect->setEnabled(capas.canDisconnect && editable);
×
773
        m_disconnect->setEnabled(capas.canDisconnect && editable);
×
774
        m_importDeviceAction->setEnabled(false);
×
775
        m_exportDeviceAction->setEnabled(true);
×
776
        m_addSiblingAction->setEnabled(false);
×
777
        m_addChildAction->setEnabled(capas.canAddNode && editable);
×
778
        m_removeNodeAction->setEnabled(editable);
×
779
        m_learnAction->setEnabled(capas.canLearn && editable);
×
780
      }
781
      m_editAction->setEnabled(editable);
×
782
    }
×
783
    else if(selection.size() > 1)
×
784
    {
785
      int selectionSize = m_ntView->selectedIndexes().size();
×
786
      bool validSelection = true;
×
787
      for(int i = 0; i < selectionSize; i++)
×
788
      {
789
        QModelIndex ind = proxyModel()->mapToSource(m_ntView->selectedIndexes().at(i));
×
790
        const auto [capas, aDeviceIsSelected]
×
791
            = getCapas(&m->nodeFromModelIndex(ind), m->deviceModel().list());
×
792
        if(!aDeviceIsSelected || !capas.canDisconnect)
×
793
        {
794
          validSelection = false;
×
795
          break;
×
796
        }
797
      }
×
798
      m_reconnect->setEnabled(validSelection && editable);
×
799
      m_disconnect->setEnabled(validSelection && editable);
×
800
    }
×
801
  }
×
802
  else
803
  {
804
    m_editAction->setEnabled(false);
595✔
805
    m_refreshAction->setEnabled(false);
595✔
806
    m_refreshValueAction->setEnabled(false);
595✔
807
    m_removeNodeAction->setEnabled(false);
595✔
808
    m_addSiblingAction->setEnabled(false);
595✔
809
    m_addChildAction->setEnabled(false);
595✔
810
    m_findUsageAction->setEnabled(false);
595✔
811
  }
812
}
1,949✔
813

814
Device::FullAddressSettings make(const Device::Node& node)
×
815
{
816
  SCORE_ASSERT(node.is<Device::AddressSettings>());
×
817
  auto& other = node.get<Device::AddressSettings>();
×
818

819
  Device::FullAddressSettings as;
×
820
  static_cast<Device::AddressSettingsCommon&>(as) = other;
×
821
  as.address = Device::address(node).address;
×
822

823
  return as;
×
824
}
×
825

826
void DeviceExplorerWidget::updateAddressView()
1,652✔
827
{
828
  auto indexes = m_ntView->selectedIndexes();
1,652✔
829

830
  if(indexes.size() != 1)
1,652✔
831
  {
832
    m_addressModel->clear();
1,652✔
833
    return;
1,652✔
834
  }
835

836
  auto& n = model()->nodeFromModelIndex(sourceIndex(indexes.first()));
×
837
  if(n.is<Device::AddressSettings>())
×
838
  {
839
    m_addressModel->setState(model(), Device::NodePath(n), make(n));
×
840
    m_addressView->resizeRowsToContents();
×
841
  }
×
842
  else
843
  {
844
    m_addressModel->clear();
×
845
  }
846
}
1,652✔
847

848
bool DeviceExplorerWidget::editable() const noexcept
595✔
849
{
850
  return m_openMenu ? m_openMenu->isEnabled() : false;
595✔
851
}
852

853
DeviceExplorerModel* DeviceExplorerWidget::model() const
2,543✔
854
{
855
  return m_ntView->model();
2,543✔
856
}
857

858
DeviceExplorerView* DeviceExplorerWidget::view() const
689✔
859
{
860
  return m_ntView;
689✔
861
}
862

863
DeviceExplorerFilterProxyModel* DeviceExplorerWidget::proxyModel()
316✔
864
{
865
  return m_proxyModel;
316✔
866
}
867

868
void DeviceExplorerWidget::edit()
×
869
{
870
  if(!editable())
×
871
    return;
×
872

873
  const auto& select = model()->nodeFromModelIndex(m_ntView->selectedIndex());
×
874
  if(select.is<Device::DeviceSettings>())
×
875
  {
876
    if(!m_deviceDialog)
×
877
    {
878
      m_deviceDialog = new DeviceEditDialog{
×
879
          *model(), m_protocolList, DeviceEditDialog::Editing, this};
×
880
    }
×
881
    auto set = select.get<Device::DeviceSettings>();
×
882
    m_deviceDialog->setSettings(set);
×
883

884
    QDialog::DialogCode code = static_cast<QDialog::DialogCode>(m_deviceDialog->exec());
×
885

886
    if(code == QDialog::Accepted)
×
887
    {
888
      auto proto = m_protocolList.get(set.protocol);
×
889
      if(!proto)
×
890
        return;
×
891
      auto flags = proto->flags();
×
892
      if(flags & Device::ProtocolFactory::EditingReloadsEverything)
×
893
      {
894
        using namespace Command;
895
        // The device is recreated from scratch with the new settings. It gets
896
        // its current tree back, not just the settings: nodes learned from
897
        // a MIDI or OSC device are user data that DeviceDocumentPlugin replays
898
        // into the new device when the protocol keeps them (canSerialize), and
899
        // discards when it builds the whole tree itself.
900
        Device::Node reloaded = select;
×
901
        reloaded.set(m_deviceDialog->getSettings());
×
902

903
        MacroCommandDispatcher<UpdateAndReloadMacro> disp{m_cmdDispatcher->stack()};
×
904
        disp.submit(new Remove(model()->deviceModel(), select));
×
905
        disp.submit(new LoadDevice(model()->deviceModel(), std::move(reloaded)));
×
906
        disp.commit();
×
907
      }
×
908
      else
909
      {
910
        auto cmd = new Explorer::Command::UpdateDeviceSettings{
×
911
            model()->deviceModel(), set.name, m_deviceDialog->getSettings()};
×
912

913
        m_cmdDispatcher->submit(cmd);
×
914
      }
915
    }
×
916

917
    updateActions();
×
918

919
    m_deviceDialog->deleteLater();
×
920
    m_deviceDialog = nullptr;
×
921
  }
×
922
  else
923
  {
924
    auto before = select.get<Device::AddressSettings>();
×
925

926
    if(!model())
×
927
      return;
×
928

929
    auto dev_s = getDevice(select);
×
930
    if(!dev_s)
×
931
      return;
×
932
    auto proto = m_protocolList.get(dev_s->protocol);
×
933
    if(!proto)
×
934
      return;
×
935
    auto dev = model()->deviceModel().list().findDevice(dev_s->name);
×
936
    if(!dev)
×
937
      return;
×
938

939
    auto dial = proto->makeEditAddressDialog(
×
940
        before, *dev, model()->deviceModel().context(), this);
×
941

942
    if(!dial)
×
943
      return;
×
944

945
    connect(
×
946
        dial, &QDialog::accepted, this,
×
947
        [this, dial, parent = select.parent(), path = Device::NodePath{select}, before] {
×
948
      auto stgs = dial->getSettings();
×
949
      // TODO do like for DeviceSettings
950
      if(!model()->checkAddressEditable(*parent, before, stgs))
×
951
        return;
×
952

953
      auto cmd = new Explorer::Command::UpdateAddressSettings{
×
954
          model()->deviceModel(), path, stgs};
×
955

956
      m_cmdDispatcher->submit(cmd);
×
957
      updateActions();
×
958
      dial->deleteLater();
×
959
        });
×
960

961
    connect(dial, &QDialog::rejected, this, [this, dial] {
×
962
      updateActions();
×
963
      dial->deleteLater();
×
964
    });
×
965

966
    dial->show();
×
967
  }
×
968
}
×
969

970
void DeviceExplorerWidget::refresh()
×
971
{
972
  if(!editable())
×
973
    return;
×
974

975
  auto m = model();
×
976
  if(!m)
×
977
    return;
×
978

979
  const auto& select = m->nodeFromModelIndex(m_ntView->selectedIndex());
×
980
  if(select.is<Device::DeviceSettings>())
×
981
  {
982
    // Create a thread, ask the device, when it is done put a command on the
983
    // chain.
984
    auto& dev
×
985
        = m->deviceModel().list().device(select.get<Device::DeviceSettings>().name);
×
986
    if(!dev.capabilities().canRefreshTree)
×
987
      return;
×
988

989
    if(!dev.connected())
×
990
      return;
×
991
    auto wrkr = make_worker(
×
992
        [this, m](Device::Node&& node) {
×
993
      auto cmd = new Explorer::Command::ReplaceDevice{
×
994
          m->deviceModel(), m_ntView->selectedIndex().row(), std::move(node)};
×
995

996
      m_cmdDispatcher->submit(cmd);
×
997
        },
×
998
        *this, dev);
×
999

1000
    wrkr->start();
×
1001
  }
×
1002
}
×
1003

1004
void DeviceExplorerWidget::refreshValue()
×
1005
{
1006
  if(!editable())
×
1007
    return;
×
1008

1009
  // TODO deprecate this
1010
  QList<QPair<const Device::Node*, ossia::value>> lst;
×
1011

1012
  auto expl = model();
×
1013

1014
  const auto& indices = m_ntView->selectedIndexes();
×
1015
  for(auto index : indices)
×
1016
  {
1017
    // Model checks
1018
    index = sourceIndex(index);
×
1019
    Device::Node* node = index.isValid()
×
1020
                             ? static_cast<Device::Node*>(index.internalPointer())
×
1021
                             : nullptr;
1022

1023
    if(!node || node->is<Device::DeviceSettings>())
×
1024
      continue;
×
1025

1026
    // Device checks
1027
    auto addr = Device::address(*node);
×
1028
    auto& dev = model()->deviceModel().list().device(addr.address.device);
×
1029
    if(!dev.capabilities().canRefreshValue)
×
1030
      return;
×
1031
    if(!dev.connected())
×
1032
      return;
×
1033

1034
    // Getting the new values
1035
    auto val = dev.refresh(addr.address);
×
1036
    if(val)
×
1037
    {
1038
      expl->editData(*node, Explorer::Column::Value, *val, Qt::EditRole);
×
1039
    }
×
1040
  }
×
1041
}
×
1042

1043
void DeviceExplorerWidget::disconnect()
×
1044
{
1045
  if(!editable())
×
1046
    return;
×
1047

1048
  auto m = model();
×
1049
  if(!m)
×
1050
    return;
×
1051
  int selectionSize = m_ntView->selectedIndexes().size();
×
1052
  for(int i = 0; i < selectionSize; i++)
×
1053
  {
1054
    const Device::Node& select = m->nodeFromModelIndex(
×
1055
        proxyModel()->mapToSource(m_ntView->selectedIndexes().at(i)));
×
1056

1057
    if(select.is<Device::DeviceSettings>())
×
1058
    {
1059
      auto& dev
×
1060
          = m->deviceModel().list().device(select.get<Device::DeviceSettings>().name);
×
1061
      dev.disconnect();
×
1062
    }
×
1063
  }
×
1064
}
×
1065

1066
void DeviceExplorerWidget::reconnect()
×
1067
{
1068
  if(!editable())
×
1069
    return;
×
1070

1071
  auto m = model();
×
1072
  if(!m)
×
1073
    return;
×
1074
  int selectionSize = m_ntView->selectedIndexes().size();
×
1075
  for(int i = 0; i < selectionSize; i++)
×
1076
  {
1077

1078
    const Device::Node& select = m->nodeFromModelIndex(
×
1079
        proxyModel()->mapToSource(m_ntView->selectedIndexes().at(i)));
×
1080
    if(select.is<Device::DeviceSettings>())
×
1081
    {
1082
      auto& dev
×
1083
          = m->deviceModel().list().device(select.get<Device::DeviceSettings>().name);
×
1084
      auto con_handle = std::make_shared<QMetaObject::Connection>();
×
1085
      *con_handle = con(
×
1086
          dev, &Device::DeviceInterface::deviceChanged, this,
×
1087
          [&dev, con_handle, select](auto oldd, auto newd) {
×
1088
        if(newd)
×
1089
        {
1090
          dev.recreate(select);
×
1091
          dev.restoreListening();
×
1092
          QObject::disconnect(*con_handle);
×
1093
        }
×
1094
          });
×
1095
      dev.reconnect();
×
1096
    }
×
1097
  }
×
1098
}
×
1099

1100
void DeviceExplorerWidget::addDevice()
×
1101
{
1102
  if(!m_deviceDialog)
×
1103
  {
1104
    m_deviceDialog = new DeviceEditDialog{
×
1105
        *model(), m_protocolList, DeviceEditDialog::Creating, this};
×
1106
  }
×
1107

1108
  connect(m_deviceDialog, &QDialog::accepted, this, [this] {
×
1109
    if(!m_deviceDialog)
×
1110
      return;
×
1111

1112
    SCORE_ASSERT(model());
×
1113
    auto node = m_deviceDialog->getDevice();
×
1114
    auto& deviceSettings = *node.target<Device::DeviceSettings>();
×
1115
    if(!model()->checkDeviceInstantiatable(deviceSettings))
×
1116
    {
1117
      m_deviceDialog->deleteLater();
×
1118
      m_deviceDialog = nullptr;
×
1119
      return;
×
1120
    }
1121
    ossia::net::sanitize_device_name(deviceSettings.name);
×
1122

1123
    auto& devplug = model()->deviceModel();
×
1124
    QTimer::singleShot(1, this, [this, n = std::move(node), &devplug]() mutable {
×
1125
      blockGUI(true);
×
1126
      m_cmdDispatcher->submit(new Command::LoadDevice{devplug, std::move(n)});
×
1127
      blockGUI(false);
×
1128
    });
×
1129

1130
    updateActions();
×
1131
    m_deviceDialog->deleteLater();
×
1132
    m_deviceDialog = nullptr;
×
1133
  });
×
1134

1135
  connect(m_deviceDialog, &QDialog::rejected, this, [this] {
×
1136
    updateActions();
×
1137
    m_deviceDialog->deleteLater();
×
1138
    m_deviceDialog = nullptr;
×
1139
  });
×
1140

1141
  m_deviceDialog->show();
×
1142
}
×
1143

1144
void DeviceExplorerWidget::importDevice()
×
1145
{
1146
  auto m = model();
×
1147
  if(!m)
×
1148
    return;
×
1149

1150
  auto fileName = QFileDialog::getOpenFileName(
×
1151
      this, tr("Device file"), score::pickerStartFolder({}, m->deviceModel().context()),
×
1152
      tr("Device file (*.device)"));
×
1153
  if(fileName.isEmpty())
×
1154
    return;
×
1155

1156
  Device::Node n;
×
1157
  if(!Device::loadDeviceFromScoreJSON(fileName, n))
×
1158
    return;
×
1159

1160
  if(!n.is<Device::DeviceSettings>())
×
1161
    return;
×
1162

1163
  auto& deviceSettings = n.get<Device::DeviceSettings>();
×
1164

1165
  // Check that this protocol is known
1166
  auto prot = m_protocolList.get(deviceSettings.protocol);
×
1167
  if(!prot)
×
1168
    return;
×
1169

1170
  if(!m->checkDeviceInstantiatable(deviceSettings))
×
1171
    return;
×
1172

1173
  ossia::net::sanitize_device_name(deviceSettings.name);
×
1174

1175
  auto& devplug = m->deviceModel();
×
1176
  blockGUI(true);
×
1177
  m_cmdDispatcher->submit(new Command::LoadDevice{devplug, std::move(n)});
×
1178
  blockGUI(false);
×
1179
}
×
1180

1181
void DeviceExplorerWidget::exportDevice()
×
1182
{
1183
  auto indexes = m_ntView->selectedIndexes();
×
1184

1185
  if(indexes.size() != 1)
×
1186
    return;
×
1187
  Device::Node& n = model()->nodeFromModelIndex(sourceIndex(indexes.first()));
×
1188
  if(!n.is<Device::DeviceSettings>())
×
1189
    return;
×
1190

1191
  auto fileName = QFileDialog::getSaveFileName(
×
1192
      this, tr("Device file"), score::pickerStartFolder({}, model()->deviceModel().context()),
×
1193
      tr("Device file (*.device)"));
×
1194
  if(!fileName.endsWith(".device"))
×
1195
    fileName.append(".device");
×
1196

1197
  QFile f{fileName};
×
1198
  if(f.open(QIODevice::WriteOnly))
×
1199
  {
1200
    f.write(toJson(n));
×
1201
  }
×
1202
}
×
1203

1204
void DeviceExplorerWidget::addChild()
×
1205
{
1206
  addAddress(InsertMode::AsChild);
×
1207
}
×
1208

1209
void DeviceExplorerWidget::addSibling()
×
1210
{
1211
  addAddress(InsertMode::AsSibling);
×
1212
}
×
1213

1214
void DeviceExplorerWidget::removeNodes()
×
1215
{
1216
  if(!editable())
×
1217
    return;
×
1218

1219
  auto indexes = m_ntView->selectedIndexes();
×
1220

1221
  Device::NodeList nodes;
×
1222
  for(auto index : indexes)
×
1223
  {
1224
    auto& n = model()->nodeFromModelIndex(sourceIndex(index));
×
1225
    if(!n.is<InvisibleRootNode>())
×
1226
      nodes.push_back(&n);
×
1227
  }
1228

1229
  auto cmd = new Command::RemoveNodes;
×
1230
  const auto& dev_model = model()->deviceModel();
×
1231

1232
  // If two nodes have the same parent,
1233
  // we should send the commands in reverse order
1234
  // (from the last to the first)
1235
  // so that they are emplaced in correct order afterwards.
1236
  // IMPORTANT ! don't use emplace, only emplace_back in D.E. model
1237
  struct PathComparator
1238
  {
1239
    bool operator()(const Device::NodePath& lhs, const Device::NodePath& rhs) const
×
1240
    {
1241
      // We iterate on the shorter.
1242
      // The shorter is considered "smaller" : it comes before.
1243
      int l_size = std::ssize(lhs);
×
1244
      int r_size = std::ssize(rhs);
×
1245
      if(l_size < r_size)
×
1246
      {
1247
        // lhs shorter
1248
        for(int i = 0; i < l_size; i++)
×
1249
        {
1250
          if(lhs[i] < rhs[i])
×
1251
            return true;
×
1252
          else if(lhs[i] == rhs[i])
×
1253
            continue;
×
1254
          else if(lhs[i] > rhs[i])
×
1255
            return false;
×
1256
        }
×
1257
        return true;
×
1258
      }
1259
      else if(l_size == r_size)
×
1260
      {
1261
        for(int i = 0; i < l_size; i++)
×
1262
        {
1263
          if(lhs[i] < rhs[i])
×
1264
            return true;
×
1265
          else if(lhs[i] == rhs[i])
×
1266
            continue;
×
1267
          else if(lhs[i] > rhs[i])
×
1268
            return false;
×
1269
        }
×
1270
        SCORE_ABORT;
×
1271
      }
×
1272
      else
1273
      {
1274
        // rhs shorter
1275
        for(int i = 0; i < r_size; i++)
×
1276
        {
1277
          if(lhs[i] < rhs[i])
×
1278
            return true;
×
1279
          else if(lhs[i] == rhs[i])
×
1280
            continue;
×
1281
          else if(lhs[i] > rhs[i])
×
1282
            return false;
×
1283
        }
×
1284
        return false;
×
1285
      }
1286

1287
      SCORE_ABORT;
×
1288
    }
×
1289
  };
1290

1291
  ossia::flat_set<Device::NodePath, PathComparator> paths;
×
1292
  for(const auto& n : filterUniqueParents(nodes))
×
1293
  {
1294
    if(n->is<Device::DeviceSettings>())
×
1295
    {
1296
      cmd->addCommand(new Explorer::Command::Remove{dev_model, *n});
×
1297
    }
×
1298
    else
1299
    {
1300
      paths.insert(*n);
×
1301
    }
1302
  }
1303
  /*
1304
      for(auto path : paths)
1305
      {
1306
          qDebug() << path;
1307
      }
1308
  */
1309
  for(auto it = paths.rbegin(); it != paths.rend(); ++it)
×
1310
  {
1311
    cmd->addCommand(new Explorer::Command::Remove{dev_model, Device::NodePath{*it}});
×
1312
  }
×
1313

1314
  m_cmdDispatcher->submit(cmd);
×
1315
}
×
1316

1317
void DeviceExplorerWidget::learn()
×
1318
{
1319
  if(!editable())
×
1320
    return;
×
1321

1322
  // Get the device
1323
  auto indexes = m_ntView->selectedIndexes();
×
1324

1325
  if(indexes.size() != 1)
×
1326
    return;
×
1327

1328
  auto m = model();
×
1329
  Device::Node& n = m->nodeFromModelIndex(sourceIndex(indexes.first()));
×
1330
  if(!n.is<Device::DeviceSettings>())
×
1331
    return;
×
1332
  auto di = m->deviceModel().list().findDevice(n.get<Device::DeviceSettings>().name);
×
1333
  if(!di)
×
1334
    return;
×
1335

1336
  if(!di->capabilities().canLearn)
×
1337
    return;
×
1338

1339
  if(!di->connected())
×
1340
    return;
×
1341

1342
  // Make a copy of the node
1343
  Device::Node oldDevice = n;
×
1344
  // Show a dialog for as long as there is learn status active
1345
  auto d = new LearnDialog{*di, this};
×
1346

1347
  auto res = d->exec();
×
1348
  delete d; // Stops learning
×
1349

1350
  // Create a command and push it if we agree, undo it and don't push it if we
1351
  // refuse
1352
  Device::Node newDevice = n;
×
1353
  if(res)
×
1354
  {
1355
    // Create a command with the current state of the device
1356
    auto cmd = new Explorer::Command::ReloadWholeDevice{
×
1357
        m->deviceModel(), std::move(oldDevice), std::move(newDevice)};
×
1358

1359
    // Push it without redoing it since the device already has the nodes
1360
    CommandDispatcher<SendStrategy::Quiet> disp{m_cmdDispatcher->stack()};
×
1361
    disp.submit(cmd);
×
1362

1363
    // This way we're able to undo the learn operation
1364
  }
×
1365
  else
1366
  {
1367
    // Take back what the learn added, one address at a time. Rolling back with
1368
    // ReloadWholeDevice::undo instead removes the device and builds a new one,
1369
    // which drops everything the outside world had attached to it -- with
1370
    // PipeWire, cancelling a learn that added nothing at all still cost the
1371
    // user their MIDI cable.
1372
    rollbackLearnedNodes(
×
1373
        m->deviceModel(), n.get<Device::DeviceSettings>().name, oldDevice);
×
1374
  }
1375
}
×
1376

1377
void DeviceExplorerWidget::findUsage()
×
1378
{
1379

1380
  auto indexes = m_ntView->selectedIndexes();
×
1381

1382
  QStringList search_txt;
×
1383
  for(auto index : indexes)
×
1384
  {
1385
    auto& n = model()->nodeFromModelIndex(sourceIndex(index));
×
1386

1387
    State::AddressAccessor address = Device::address(n);
×
1388

1389
    search_txt.push_back(address.address.toString());
×
1390
  }
×
1391
  findAddresses(search_txt);
×
1392
}
×
1393

1394
void DeviceExplorerWidget::do_addAddress(
×
1395
    InsertMode insert, QModelIndex index, Device::Node* parent,
1396
    Device::AddressSettings& stgs)
1397
{
1398
  // TODO checking for expansion should not be necessary anymore
1399
  const bool parent_is_expanded = m_ntView->isExpanded(
×
1400
      proxyIndex(m_ntView->model()->modelIndexFromNode(*parent, 0)));
×
1401

1402
  m_cmdDispatcher->submit(new Explorer::Command::AddAddress{
×
1403
      model()->deviceModel(), Device::NodePath{index}, insert, stgs});
×
1404

1405
  // If the node is going to be visible, we have to start listening to it.
1406
  if(parent_is_expanded && m_listeningManager)
×
1407
  {
1408
    auto child_it = ossia::find_if(*parent, [&](const Device::Node& child) {
×
1409
      return child.get<Device::AddressSettings>().name == stgs.name;
×
1410
    });
1411
    SCORE_ASSERT(child_it != parent->end());
×
1412

1413
    m_listeningManager->enableListening(*child_it);
×
1414
  }
×
1415
}
×
1416

1417
void DeviceExplorerWidget::addAddress(InsertMode insert)
×
1418
{
1419
  if(!editable())
×
1420
    return;
×
1421

1422
  SCORE_ASSERT(model());
×
1423
  QModelIndex index = proxyModel()->mapToSource(m_ntView->currentIndex());
×
1424

1425
  // If the node is added in sibling mode, we check that no sibling have
1426
  // the same name
1427
  // Else we check that no child of the index has the same name.
1428
  auto& node = model()->nodeFromModelIndex(index);
×
1429

1430
  // TODO not very elegant.
1431
  if(insert == InsertMode::AsSibling && node.is<Device::DeviceSettings>())
×
1432
  {
1433
    return;
×
1434
  }
1435

1436
  auto dev_s = getDevice(node);
×
1437
  if(!dev_s)
×
1438
    return;
×
1439
  auto proto = m_protocolList.get(dev_s->protocol);
×
1440
  if(!proto)
×
1441
    return;
×
1442
  auto dev = model()->deviceModel().list().findDevice(dev_s->name);
×
1443
  if(!dev)
×
1444
    return;
×
1445

1446
  QScopedPointer<Device::AddressDialog> dial{
×
1447
      proto->makeAddAddressDialog(*dev, model()->deviceModel().context(), this)};
×
1448

1449
  if(!dial)
×
1450
    return;
×
1451

1452
  dial->setParent(this);
×
1453
  auto code = static_cast<QDialog::DialogCode>(dial->exec());
×
1454

1455
  if(code == QDialog::Accepted)
×
1456
  {
1457
    Device::Node* parent = (insert == InsertMode::AsChild) ? &node : node.parent();
×
1458

1459
    auto stgs = dial->getSettings();
×
1460

1461
    // Clean-up the address entered by the user with some common typos
1462
    stgs.name = stgs.name.trimmed();
×
1463
    if(stgs.name.startsWith('/'))
×
1464
      stgs.name.remove(0, 1);
×
1465
    while(stgs.name.contains(QLatin1String("//")))
×
1466
      stgs.name.remove(QLatin1String("//"));
×
1467
    stgs.name = stgs.name.trimmed();
×
1468

1469
    auto addr_request = stgs.name.toStdString();
×
1470
    auto [names, is_brace_exp] = ossia::net::expand_address(addr_request);
×
1471
    if(is_brace_exp || stgs.name.contains('/'))
×
1472
    {
1473
      std::vector<QString> correct_names;
×
1474
      for(auto& name : names)
×
1475
      {
1476
        stgs.name = QString::fromStdString(name);
×
1477
        if(model()->checkAddressInstantiatable(*parent, stgs))
×
1478
        {
1479
          correct_names.push_back(std::move(stgs.name));
×
1480
        }
×
1481
      }
1482

1483
      if(!correct_names.empty())
×
1484
      {
1485
        // bool parent_is_expanded = m_ntView->isExpanded(
1486
        //     proxyIndex(m_ntView->model()->modelIndexFromNode(*parent, 0)));
1487

1488
        auto cmd = new Explorer::Command::AddAddresses{};
×
1489
        for(auto& name : correct_names)
×
1490
        {
1491
          stgs.name = name;
×
1492
          cmd->addCommand(new Explorer::Command::AddAddress{
×
1493
              model()->deviceModel(), Device::NodePath{index}, insert, stgs});
×
1494
        }
1495
        this->m_cmdDispatcher->submit(cmd);
×
1496

1497
        // If the node is going to be visible, we have to start listening to it.
1498
        // FIXME
1499
        // if (parent_is_expanded && m_listeningManager)
1500
        // {
1501
        //   for(auto& name : correct_names)
1502
        //   {
1503
        //     auto child_it = ossia::find_if(*parent, [&](const Device::Node& child) {
1504
        //       return child.get<Device::AddressSettings>().name == stgs.name;
1505
        //     });
1506
        //     SCORE_ASSERT(child_it != parent->end());
1507
        //
1508
        //     m_listeningManager->enableListening(*child_it);
1509
        //   }
1510
        // }
1511
      }
×
1512
    }
×
1513
    else
1514
    {
1515
      if(!model()->checkAddressInstantiatable(*parent, stgs))
×
1516
        return;
×
1517

1518
      do_addAddress(insert, index, parent, stgs);
×
1519
    }
1520
    updateActions();
×
1521
  }
×
1522
}
×
1523

1524
DeviceExplorerWidget*
1525
findDeviceExplorerWidgetInstance(const score::GUIApplicationContext& ctx) noexcept
2,007✔
1526
{
1527
  // findPanel tolerates empty slots in the panel list (indirect iteration
1528
  // dereferences unconditionally and asserts on hardened stdlibs).
1529
  if(auto* panel = ctx.components.findPanel<Explorer::PanelDelegate>())
2,007✔
1530
  {
1531
    return static_cast<Explorer::DeviceExplorerWidget*>(panel->widget());
1,672✔
1532
  }
1533
  return nullptr;
335✔
1534
}
2,007✔
1535

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