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

ossia / score / 33560887155

01 Sep 2026 09:24PM UTC coverage: 24.648% (+0.7%) from 23.997%
33560887155

Pull #2179

github

web-flow
Merge ecc0a2ad3 into 50d28933c
Pull Request #2179: Survive a build that does not have every plug-in

408 of 757 new or added lines in 47 files covered. (53.9%)

15 existing lines in 12 files now uncovered.

55851 of 226593 relevant lines covered (24.65%)

61526.39 hits per line

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

36.62
/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/Commands/Remove.hpp>
25
#include <Explorer/Commands/RemoveNodes.hpp>
26
#include <Explorer/Commands/ReplaceDevice.hpp>
27
#include <Explorer/Commands/Update/UpdateAddressSettings.hpp>
28
#include <Explorer/Commands/Update/UpdateDeviceSettings.hpp>
29
#include <Explorer/DeviceList.hpp>
30
#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
31
#include <Explorer/Explorer/AddressItemModel.hpp>
32
#include <Explorer/Explorer/DeviceExplorerDelegate.hpp>
33
#include <Explorer/Explorer/DeviceExplorerModel.hpp>
34
#include <Explorer/Listening/ListeningHandler.hpp>
35
#include <Explorer/Panel/DeviceExplorerPanelDelegate.hpp>
36

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

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

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

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

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

92
#include <wobjectimpl.h>
93

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

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

123
public:
124
  ExplorerSearchLineEdit(DeviceExplorerWidget& parent)
216 ✔
125
      : score::SearchLineEdit{&parent}
108 ✔
126
      , m_widget{parent}
108 ✔
127
  {
108 ✔
128
    connect(this, &QLineEdit::textEdited, this, [this] { search(); });
108 ✔
129
  }
108 ✔
130

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

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

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

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

164
  DeviceExplorerWidget& m_widget;
165
};
166

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

301
  m_disconnect = new QAction{tr("Disconnect"), this};
108 ✔
302
  m_reconnect = new QAction{tr("Reconnect"), this};
108 ✔
303

304
  m_refreshValueAction = new QAction(tr("Refresh value"), this);
108 ✔
305

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

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

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

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

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

359
  m_openMenu = new QToolButton(this);
108 ✔
360
  m_openMenu->setIcon(makeIcons(
216 ✔
361
      QStringLiteral(":/icons/add_on.png"), QStringLiteral(":/icons/add_off.png"),
108 ✔
362
      QStringLiteral(":/icons/add_disabled.png")));
108 ✔
363
  m_openMenu->setAutoRaise(true);
108 ✔
364

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

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

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

389
  m_addSiblingAction->setEnabled(false);
108 ✔
390
  m_addChildAction->setEnabled(false);
108 ✔
391

392
  // Setup menus
393

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

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

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

415
    this->addAction(m_refreshAction);
108 ✔
416
    this->addAction(m_refreshValueAction);
108 ✔
417
    this->addAction(m_learnAction);
108 ✔
418
    this->addAction(m_findUsageAction);
108 ✔
419

420
    this->addAction(m_removeNodeAction);
108 ✔
421
  }
422

423
  m_columnCBox = new QComboBox(this);
108 ✔
424
  m_nameLEdit = new ExplorerSearchLineEdit(*this);
108 ✔
425

426
  connect(
108 ✔
427
      m_columnCBox, SignalUtils::QComboBox_currentIndexChanged_int(), m_nameLEdit,
108 ✔
428
      [this] {
210 ✔
429
    m_proxyModel->setColumn((Explorer::Column)m_columnCBox->currentIndex());
102 ✔
430
    m_nameLEdit->search();
102 ✔
431
      });
102 ✔
432

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

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

448
  m_lay = new QStackedLayout;
108 ✔
449
  m_lay->addWidget(mainWidg);
108 ✔
450

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

459
  refreshLay->addWidget(m_refreshIndicator);
108 ✔
460
  m_lay->addWidget(refreshParent);
108 ✔
461
  setLayout(m_lay);
108 ✔
462
}
108 ✔
463

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

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

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

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

505
QSize DeviceExplorerWidget::sizeHint() const
×
506
{
507
  return {200, 800};
×
508
}
509

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

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

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

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

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

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

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

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

569
  if(model)
295 ✔
570
  {
571
    m_proxyModel = new DeviceExplorerFilterProxyModel(this);
96 ✔
572
    m_proxyModel->setSourceModel(model);
96 ✔
573
    m_ntView->setModel(m_proxyModel);
96 ✔
574
    model->setView(m_ntView);
96 ✔
575

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

579
    populateColumnCBox();
96 ✔
580

581
    updateActions();
96 ✔
582

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

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

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

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

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

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

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

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

682
        if(selected.parent() == topLeft.parent() && selected.row() == topLeft.row())
×
683
          updateAddressView();
×
684
      }
×
685
        });
34 ✔
686
  }
96 ✔
687
  else
688
  {
689
    m_ntView->setModel((QAbstractItemModel*)nullptr);
199 ✔
690
  }
691

692
  setEnabled(bool(model));
295 ✔
693
}
295 ✔
694

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

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

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

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

723
void DeviceExplorerWidget::updateActions()
391 ✔
724
{
725
  auto m = model();
391 ✔
726
  if(!m)
391 ✔
727
    return;
198 ✔
728

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

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

748
    SCORE_ASSERT(m_ntView);
×
749

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

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

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

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

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

822
  return as;
×
823
}
×
824

825
void DeviceExplorerWidget::updateAddressView()
295 ✔
826
{
827
  auto indexes = m_ntView->selectedIndexes();
295 ✔
828

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

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

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

852
DeviceExplorerModel* DeviceExplorerWidget::model() const
583 ✔
853
{
854
  return m_ntView->model();
583 ✔
855
}
856

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

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

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

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

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

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

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

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

916
    updateActions();
×
917

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
988
    auto& dev = *dev_p;
×
989
    if(!dev.capabilities().canRefreshTree)
×
990
      return;
×
991

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

999
      m_cmdDispatcher->submit(cmd);
×
1000
        },
×
1001
        *this, dev);
×
1002

1003
    wrkr->start();
×
1004
  }
×
1005
}
×
1006

1007
void DeviceExplorerWidget::refreshValue()
×
1008
{
1009
  if(!editable())
×
1010
    return;
×
1011

1012
  // TODO deprecate this
1013
  QList<QPair<const Device::Node*, ossia::value>> lst;
×
1014

1015
  auto expl = model();
×
1016

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

1026
    if(!node || node->is<Device::DeviceSettings>())
×
1027
      continue;
×
1028

1029
    // Device checks
1030
    auto addr = Device::address(*node);
×
NEW
1031
    auto* dev_p = model()->deviceModel().list().findDevice(addr.address.device);
×
NEW
1032
    if(!dev_p)
×
NEW
1033
      return;
×
1034

NEW
1035
    auto& dev = *dev_p;
×
1036
    if(!dev.capabilities().canRefreshValue)
×
1037
      return;
×
1038
    if(!dev.connected())
×
1039
      return;
×
1040

1041
    // Getting the new values
1042
    auto val = dev.refresh(addr.address);
×
1043
    if(val)
×
1044
    {
1045
      expl->editData(*node, Explorer::Column::Value, *val, Qt::EditRole);
×
1046
    }
×
1047
  }
×
1048
}
×
1049

1050
void DeviceExplorerWidget::disconnect()
×
1051
{
1052
  if(!editable())
×
1053
    return;
×
1054

1055
  auto m = model();
×
1056
  if(!m)
×
1057
    return;
×
1058
  int selectionSize = m_ntView->selectedIndexes().size();
×
1059
  for(int i = 0; i < selectionSize; i++)
×
1060
  {
1061
    const Device::Node& select = m->nodeFromModelIndex(
×
1062
        proxyModel()->mapToSource(m_ntView->selectedIndexes().at(i)));
×
1063

1064
    if(select.is<Device::DeviceSettings>())
×
1065
    {
NEW
1066
      if(auto* dev = m->deviceModel().list().findDevice(
×
NEW
1067
             select.get<Device::DeviceSettings>().name))
×
NEW
1068
        dev->disconnect();
×
1069
    }
×
1070
  }
×
1071
}
×
1072

1073
void DeviceExplorerWidget::reconnect()
×
1074
{
1075
  if(!editable())
×
1076
    return;
×
1077

1078
  auto m = model();
×
1079
  if(!m)
×
1080
    return;
×
1081
  int selectionSize = m_ntView->selectedIndexes().size();
×
1082
  for(int i = 0; i < selectionSize; i++)
×
1083
  {
1084

1085
    const Device::Node& select = m->nodeFromModelIndex(
×
1086
        proxyModel()->mapToSource(m_ntView->selectedIndexes().at(i)));
×
1087
    if(select.is<Device::DeviceSettings>())
×
1088
    {
NEW
1089
      auto* dev_p
×
NEW
1090
          = m->deviceModel().list().findDevice(select.get<Device::DeviceSettings>().name);
×
NEW
1091
      if(!dev_p)
×
NEW
1092
        continue;
×
1093

NEW
1094
      auto& dev = *dev_p;
×
1095
      auto con_handle = std::make_shared<QMetaObject::Connection>();
×
1096
      *con_handle = con(
×
1097
          dev, &Device::DeviceInterface::deviceChanged, this,
×
1098
          [&dev, con_handle, select](auto oldd, auto newd) {
×
1099
        if(newd)
×
1100
        {
1101
          dev.recreate(select);
×
1102
          dev.restoreListening();
×
1103
          QObject::disconnect(*con_handle);
×
1104
        }
×
1105
          });
×
1106
      dev.reconnect();
×
1107
    }
×
1108
  }
×
1109
}
×
1110

1111
void DeviceExplorerWidget::addDevice()
×
1112
{
1113
  if(!m_deviceDialog)
×
1114
  {
1115
    m_deviceDialog = new DeviceEditDialog{
×
1116
        *model(), m_protocolList, DeviceEditDialog::Creating, this};
×
1117
  }
×
1118

1119
  connect(m_deviceDialog, &QDialog::accepted, this, [this] {
×
1120
    if(!m_deviceDialog)
×
1121
      return;
×
1122

1123
    SCORE_ASSERT(model());
×
1124
    auto node = m_deviceDialog->getDevice();
×
1125
    auto& deviceSettings = *node.target<Device::DeviceSettings>();
×
1126
    if(!model()->checkDeviceInstantiatable(deviceSettings))
×
1127
    {
1128
      m_deviceDialog->deleteLater();
×
1129
      m_deviceDialog = nullptr;
×
1130
      return;
×
1131
    }
1132
    ossia::net::sanitize_device_name(deviceSettings.name);
×
1133

1134
    auto& devplug = model()->deviceModel();
×
1135
    QTimer::singleShot(1, this, [this, n = std::move(node), &devplug]() mutable {
×
1136
      blockGUI(true);
×
1137
      m_cmdDispatcher->submit(new Command::LoadDevice{devplug, std::move(n)});
×
1138
      blockGUI(false);
×
1139
    });
×
1140

1141
    updateActions();
×
1142
    m_deviceDialog->deleteLater();
×
1143
    m_deviceDialog = nullptr;
×
1144
  });
×
1145

1146
  connect(m_deviceDialog, &QDialog::rejected, this, [this] {
×
1147
    updateActions();
×
1148
    m_deviceDialog->deleteLater();
×
1149
    m_deviceDialog = nullptr;
×
1150
  });
×
1151

1152
  m_deviceDialog->show();
×
1153
}
×
1154

1155
void DeviceExplorerWidget::importDevice()
×
1156
{
1157
  auto m = model();
×
1158
  if(!m)
×
1159
    return;
×
1160

1161
  auto fileName = QFileDialog::getOpenFileName(
×
1162
      this, tr("Device file"), score::pickerStartFolder({}, m->deviceModel().context()),
×
1163
      tr("Device file (*.device)"));
×
1164
  if(fileName.isEmpty())
×
1165
    return;
×
1166

1167
  Device::Node n;
×
1168
  if(!Device::loadDeviceFromScoreJSON(fileName, n))
×
1169
    return;
×
1170

1171
  if(!n.is<Device::DeviceSettings>())
×
1172
    return;
×
1173

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

1176
  // Check that this protocol is known
1177
  auto prot = m_protocolList.get(deviceSettings.protocol);
×
1178
  if(!prot)
×
1179
    return;
×
1180

1181
  if(!m->checkDeviceInstantiatable(deviceSettings))
×
1182
    return;
×
1183

1184
  ossia::net::sanitize_device_name(deviceSettings.name);
×
1185

1186
  auto& devplug = m->deviceModel();
×
1187
  blockGUI(true);
×
1188
  m_cmdDispatcher->submit(new Command::LoadDevice{devplug, std::move(n)});
×
1189
  blockGUI(false);
×
1190
}
×
1191

1192
void DeviceExplorerWidget::exportDevice()
×
1193
{
1194
  auto indexes = m_ntView->selectedIndexes();
×
1195

1196
  if(indexes.size() != 1)
×
1197
    return;
×
1198
  Device::Node& n = model()->nodeFromModelIndex(sourceIndex(indexes.first()));
×
1199
  if(!n.is<Device::DeviceSettings>())
×
1200
    return;
×
1201

1202
  auto fileName = QFileDialog::getSaveFileName(
×
1203
      this, tr("Device file"), score::pickerStartFolder({}, model()->deviceModel().context()),
×
1204
      tr("Device file (*.device)"));
×
1205
  if(!fileName.endsWith(".device"))
×
1206
    fileName.append(".device");
×
1207

1208
  QFile f{fileName};
×
1209
  if(f.open(QIODevice::WriteOnly))
×
1210
  {
1211
    f.write(toJson(n));
×
1212
  }
×
1213
}
×
1214

1215
void DeviceExplorerWidget::addChild()
×
1216
{
1217
  addAddress(InsertMode::AsChild);
×
1218
}
×
1219

1220
void DeviceExplorerWidget::addSibling()
×
1221
{
1222
  addAddress(InsertMode::AsSibling);
×
1223
}
×
1224

1225
void DeviceExplorerWidget::removeNodes()
×
1226
{
1227
  if(!editable())
×
1228
    return;
×
1229

1230
  auto indexes = m_ntView->selectedIndexes();
×
1231

1232
  Device::NodeList nodes;
×
1233
  for(auto index : indexes)
×
1234
  {
1235
    auto& n = model()->nodeFromModelIndex(sourceIndex(index));
×
1236
    if(!n.is<InvisibleRootNode>())
×
1237
      nodes.push_back(&n);
×
1238
  }
1239

1240
  auto cmd = new Command::RemoveNodes;
×
1241
  const auto& dev_model = model()->deviceModel();
×
1242

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

1298
      SCORE_ABORT;
×
1299
    }
×
1300
  };
1301

1302
  ossia::flat_set<Device::NodePath, PathComparator> paths;
×
1303
  for(const auto& n : filterUniqueParents(nodes))
×
1304
  {
1305
    if(n->is<Device::DeviceSettings>())
×
1306
    {
1307
      cmd->addCommand(new Explorer::Command::Remove{dev_model, *n});
×
1308
    }
×
1309
    else
1310
    {
1311
      paths.insert(*n);
×
1312
    }
1313
  }
1314
  /*
1315
      for(auto path : paths)
1316
      {
1317
          qDebug() << path;
1318
      }
1319
  */
1320
  for(auto it = paths.rbegin(); it != paths.rend(); ++it)
×
1321
  {
1322
    cmd->addCommand(new Explorer::Command::Remove{dev_model, Device::NodePath{*it}});
×
1323
  }
×
1324

1325
  m_cmdDispatcher->submit(cmd);
×
1326
}
×
1327

1328
void DeviceExplorerWidget::learn()
×
1329
{
1330
  if(!editable())
×
1331
    return;
×
1332

1333
  // Get the device
1334
  auto indexes = m_ntView->selectedIndexes();
×
1335

1336
  if(indexes.size() != 1)
×
1337
    return;
×
1338

1339
  auto m = model();
×
1340
  Device::Node& n = m->nodeFromModelIndex(sourceIndex(indexes.first()));
×
1341
  if(!n.is<Device::DeviceSettings>())
×
1342
    return;
×
1343
  auto di = m->deviceModel().list().findDevice(n.get<Device::DeviceSettings>().name);
×
1344
  if(!di)
×
1345
    return;
×
1346

1347
  if(!di->capabilities().canLearn)
×
1348
    return;
×
1349

1350
  if(!di->connected())
×
1351
    return;
×
1352

1353
  // Make a copy of the node
1354
  Device::Node oldDevice = n;
×
1355
  // Show a dialog for as long as there is learn status active
1356
  auto d = new LearnDialog{*di, this};
×
1357

1358
  auto res = d->exec();
×
1359
  delete d; // Stops learning
×
1360

1361
  // Create a command and push it if we agree, undo it and don't push it if we
1362
  // refuse
1363
  Device::Node newDevice = n;
×
1364
  if(res)
×
1365
  {
1366
    // Create a command with the current state of the device
1367
    auto cmd = new Explorer::Command::ReloadWholeDevice{
×
1368
        m->deviceModel(), std::move(oldDevice), std::move(newDevice)};
×
1369

1370
    // Push it without redoing it since the device already has the nodes
1371
    CommandDispatcher<SendStrategy::Quiet> disp{m_cmdDispatcher->stack()};
×
1372
    disp.submit(cmd);
×
1373

1374
    // This way we're able to undo the learn operation
1375
  }
×
1376
  else
1377
  {
1378
    // We still have to rollback the messages that may have been received
1379
    Explorer::Command::ReloadWholeDevice cmd{
×
1380
        m->deviceModel(), std::move(oldDevice), std::move(newDevice)};
×
1381

1382
    // No need to push anything
1383
    cmd.undo(m_cmdDispatcher->stack().context());
×
1384
  }
×
1385
}
×
1386

1387
void DeviceExplorerWidget::findUsage()
×
1388
{
1389

1390
  auto indexes = m_ntView->selectedIndexes();
×
1391

1392
  QStringList search_txt;
×
1393
  for(auto index : indexes)
×
1394
  {
1395
    auto& n = model()->nodeFromModelIndex(sourceIndex(index));
×
1396

1397
    State::AddressAccessor address = Device::address(n);
×
1398

1399
    search_txt.push_back(address.address.toString());
×
1400
  }
×
1401
  findAddresses(search_txt);
×
1402
}
×
1403

1404
void DeviceExplorerWidget::do_addAddress(
×
1405
    InsertMode insert, QModelIndex index, Device::Node* parent,
1406
    Device::AddressSettings& stgs)
1407
{
1408
  // TODO checking for expansion should not be necessary anymore
1409
  const bool parent_is_expanded = m_ntView->isExpanded(
×
1410
      proxyIndex(m_ntView->model()->modelIndexFromNode(*parent, 0)));
×
1411

1412
  m_cmdDispatcher->submit(new Explorer::Command::AddAddress{
×
1413
      model()->deviceModel(), Device::NodePath{index}, insert, stgs});
×
1414

1415
  // If the node is going to be visible, we have to start listening to it.
1416
  if(parent_is_expanded && m_listeningManager)
×
1417
  {
1418
    auto child_it = ossia::find_if(*parent, [&](const Device::Node& child) {
×
1419
      return child.get<Device::AddressSettings>().name == stgs.name;
×
1420
    });
1421
    SCORE_ASSERT(child_it != parent->end());
×
1422

1423
    m_listeningManager->enableListening(*child_it);
×
1424
  }
×
1425
}
×
1426

1427
void DeviceExplorerWidget::addAddress(InsertMode insert)
×
1428
{
1429
  if(!editable())
×
1430
    return;
×
1431

1432
  SCORE_ASSERT(model());
×
1433
  QModelIndex index = proxyModel()->mapToSource(m_ntView->currentIndex());
×
1434

1435
  // If the node is added in sibling mode, we check that no sibling have
1436
  // the same name
1437
  // Else we check that no child of the index has the same name.
1438
  auto& node = model()->nodeFromModelIndex(index);
×
1439

1440
  // TODO not very elegant.
1441
  if(insert == InsertMode::AsSibling && node.is<Device::DeviceSettings>())
×
1442
  {
1443
    return;
×
1444
  }
1445

1446
  auto dev_s = getDevice(node);
×
1447
  if(!dev_s)
×
1448
    return;
×
1449
  auto proto = m_protocolList.get(dev_s->protocol);
×
1450
  if(!proto)
×
1451
    return;
×
1452
  auto dev = model()->deviceModel().list().findDevice(dev_s->name);
×
1453
  if(!dev)
×
1454
    return;
×
1455

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

1459
  if(!dial)
×
1460
    return;
×
1461

1462
  dial->setParent(this);
×
1463
  auto code = static_cast<QDialog::DialogCode>(dial->exec());
×
1464

1465
  if(code == QDialog::Accepted)
×
1466
  {
1467
    Device::Node* parent = (insert == InsertMode::AsChild) ? &node : node.parent();
×
1468

1469
    auto stgs = dial->getSettings();
×
1470

1471
    // Clean-up the address entered by the user with some common typos
1472
    stgs.name = stgs.name.trimmed();
×
1473
    if(stgs.name.startsWith('/'))
×
1474
      stgs.name.remove(0, 1);
×
1475
    while(stgs.name.contains(QLatin1String("//")))
×
1476
      stgs.name.remove(QLatin1String("//"));
×
1477
    stgs.name = stgs.name.trimmed();
×
1478

1479
    auto addr_request = stgs.name.toStdString();
×
1480
    auto [names, is_brace_exp] = ossia::net::expand_address(addr_request);
×
1481
    if(is_brace_exp || stgs.name.contains('/'))
×
1482
    {
1483
      std::vector<QString> correct_names;
×
1484
      for(auto& name : names)
×
1485
      {
1486
        stgs.name = QString::fromStdString(name);
×
1487
        if(model()->checkAddressInstantiatable(*parent, stgs))
×
1488
        {
1489
          correct_names.push_back(std::move(stgs.name));
×
1490
        }
×
1491
      }
1492

1493
      if(!correct_names.empty())
×
1494
      {
1495
        // bool parent_is_expanded = m_ntView->isExpanded(
1496
        //     proxyIndex(m_ntView->model()->modelIndexFromNode(*parent, 0)));
1497

1498
        auto cmd = new Explorer::Command::AddAddresses{};
×
1499
        for(auto& name : correct_names)
×
1500
        {
1501
          stgs.name = name;
×
1502
          cmd->addCommand(new Explorer::Command::AddAddress{
×
1503
              model()->deviceModel(), Device::NodePath{index}, insert, stgs});
×
1504
        }
1505
        this->m_cmdDispatcher->submit(cmd);
×
1506

1507
        // If the node is going to be visible, we have to start listening to it.
1508
        // FIXME
1509
        // if (parent_is_expanded && m_listeningManager)
1510
        // {
1511
        //   for(auto& name : correct_names)
1512
        //   {
1513
        //     auto child_it = ossia::find_if(*parent, [&](const Device::Node& child) {
1514
        //       return child.get<Device::AddressSettings>().name == stgs.name;
1515
        //     });
1516
        //     SCORE_ASSERT(child_it != parent->end());
1517
        //
1518
        //     m_listeningManager->enableListening(*child_it);
1519
        //   }
1520
        // }
1521
      }
×
1522
    }
×
1523
    else
1524
    {
1525
      if(!model()->checkAddressInstantiatable(*parent, stgs))
×
1526
        return;
×
1527

1528
      do_addAddress(insert, index, parent, stgs);
×
1529
    }
1530
    updateActions();
×
1531
  }
×
1532
}
×
1533

1534
DeviceExplorerWidget*
1535
findDeviceExplorerWidgetInstance(const score::GUIApplicationContext& ctx) noexcept
417 ✔
1536
{
1537
  // findPanel tolerates empty slots in the panel list (indirect iteration
1538
  // dereferences unconditionally and asserts on hardened stdlibs).
1539
  if(auto* panel = ctx.components.findPanel<Explorer::PanelDelegate>())
417 ✔
1540
  {
1541
    return static_cast<Explorer::DeviceExplorerWidget*>(panel->widget());
291 ✔
1542
  }
1543
  return nullptr;
126 ✔
1544
}
417 ✔
1545

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