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

ossia / score / 31920334674

16 Aug 2026 01:46AM UTC coverage: 16.231% (+0.7%) from 15.533%
31920334674

push

github

jcelerier
js: add Score.select()

selectedObject() / selectedObjects() had no counterpart, so a script could read
the selection but not set it - which leaves the selection-driven editing paths
(creating a process after the selected one, for instance) unreachable from a
script. Takes a single object or a list; anything that is not an
IdentifiedObjectAbstract clears the selection.

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

0 of 19 new or added lines in 1 file covered. (0.0%)

466 existing lines in 8 files now uncovered.

33273 of 204995 relevant lines covered (16.23%)

1955.02 hits per line

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

42.8
/src/plugins/score-lib-process/Process/Dataflow/NodeItem.cpp
1
#include <Process/Commands/Properties.hpp>
2
#include <Process/Dataflow/NodeItem.hpp>
3
#include <Process/Dataflow/PortFactory.hpp>
4
#include <Process/Dataflow/PortItem.hpp>
5
#include <Process/Dataflow/PortVisibility.hpp>
6
#include <Process/Focus/FocusDispatcher.hpp>
7
#include <Process/Process.hpp>
8
#include <Process/ProcessFactory.hpp>
9
#include <Process/ProcessList.hpp>
10
#include <Process/Style/Pixmaps.hpp>
11
#include <Process/Style/ScenarioStyle.hpp>
12

13
#include <Control/DefaultEffectItem.hpp>
14
#include <Effect/EffectLayer.hpp>
15
#include <Effect/EffectPainting.hpp>
16

17
#include <Process/ApplicationPlugin.hpp>
18
#include <score/application/GUIApplicationContext.hpp>
19
#include <score/command/Dispatchers/CommandDispatcher.hpp>
20
#include <score/document/DocumentContext.hpp>
21
#include <score/graphics/GraphicWidgets.hpp>
22
#include <score/graphics/GraphicsLayout.hpp>
23
#include <score/graphics/TextItem.hpp>
24
#include <score/model/Skin.hpp>
25
#include <score/selection/Selection.hpp>
26
#include <score/selection/SelectionDispatcher.hpp>
27
#include <score/selection/SelectionStack.hpp>
28
#include <score/tools/Bind.hpp>
29

30
#include <ossia/detail/ssize.hpp>
31

32
#include <ossia-qt/invoke.hpp>
33

34
#include <QCursor>
35
#include <QGraphicsScene>
36
#include <QGraphicsSceneMouseEvent>
37
#include <QKeyEvent>
38
#include <QMenu>
39
#include <QPainter>
40
#include <QTimer>
41

42
#include <wobjectimpl.h>
43
W_OBJECT_IMPL(Process::NodeItem)
5✔
44
namespace Process
45
{
46

47
static const constexpr qreal TitleHeight = 20.;
48
//static const constexpr qreal TitleX0 = 15;
49
static const constexpr qreal TitleY0 = -TitleHeight + 2.;
50
static const constexpr qreal FoldX0 = 2;
51
static const constexpr qreal FoldY0 = TitleY0 + 1;
52
static const constexpr qreal UiX0 = 16;
53
static const constexpr qreal UiY0 = TitleY0 + 1.;
54
static const constexpr qreal TitleWithUiX0 = 27;
55
static const constexpr qreal SpacingIcon = 3;
56

57
static const constexpr qreal FooterHeight = 0.;
58
static const constexpr qreal Corner = 2.;
59
static const constexpr qreal PortSpacing = 10.;
60
static const constexpr qreal InletX0 = -10.;
61
static const constexpr qreal InletY0 = 0;
62
static const constexpr qreal OutletX0 = -2.;
63
static const constexpr qreal OutletY0 = InletY0; // Add to height
64
static const constexpr qreal TopButtonX0 = -12.;
65
static const constexpr qreal TopButtonY0 = TitleY0 + 2.;
66
static const constexpr qreal LeftSideWidth = 10.;
67
static const constexpr qreal RightSideWidth = LeftSideWidth;
68

69
NodeItem::NodeItem(
6✔
70
    const Process::ProcessModel& process, const Process::Context& ctx, TimeVal parentDur,
71
    QGraphicsItem* parent)
72
    : QGraphicsItem{parent}
6✔
73
    , m_model{process}
74
    , m_context{ctx}
75
    , m_dispatcher{ctx.commandStack}
76
    , m_parentDuration{parentDur}
77
{
6✔
78
  setObjectName("NodeItem");
6✔
79
  setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
6✔
80
  setAcceptHoverEvents(true);
6✔
81
  setAcceptDrops(true);
6✔
82
  setFlag(ItemIsFocusable, true);
6✔
83
  setFlag(ItemClipsChildrenToShape, true);
6✔
84
  const auto& pf
6✔
85
      = ctx.app.interfaces<Process::ProcessFactoryList>().get(process.concreteKey());
6✔
86
  setData(0xF1, pf->descriptor(process).documentationLink);
6✔
87

88
  if(process.flags() & Process::ProcessFlags::FullyCustomItem)
6✔
89
  {
UNCOV
90
    createWithoutDecorations();
×
UNCOV
91
  }
×
92
  else
93
  {
94
    createWithDecorations();
6✔
95
  }
96

97
  initConnections();
6✔
UNCOV
98
}
×
99

100
void NodeItem::createWithDecorations()
6✔
101
{
102
  auto& process = m_model;
6✔
103
  auto& ctx = m_context;
6✔
104

105
  m_folded = (std::ssize(process.inlets()) > Process::MaxUnpaginatedControls);
6✔
106

107
  // Title
108
  const auto& pixmaps = Process::Pixmaps::instance();
6✔
109
  m_fold
12✔
110
      = new score::QGraphicsPixmapToggle{pixmaps.unroll_small, pixmaps.roll_small, this};
6✔
111
  m_fold->setState(!m_folded);
6✔
112

113
  m_scriptButton = Process::makeScriptButton(
6✔
114
      const_cast<Process::ProcessModel&>(process), ctx, this, this);
6✔
115
  m_uiButton = Process::makeExternalUIButton(
6✔
116
      const_cast<Process::ProcessModel&>(process), ctx, this, this);
6✔
117
  m_presetButton = Process::makePresetButton(process, ctx, this, this);
6✔
118

119
  auto& skin = score::Skin::instance();
6✔
120
  m_label = new score::SimpleTextItem{skin.Light.main, this};
6✔
121
  m_label->setFont(skin.Bold10Pt);
6✔
122

123
  updateLabel();
6✔
124

125
  con(process.metadata(), &score::ModelMetadata::NameChanged, this,
12✔
126
      &NodeItem::updateLabel);
6✔
127
  con(process.metadata(), &score::ModelMetadata::LabelChanged, this,
12✔
128
      &NodeItem::updateLabel);
6✔
129

130
  con(process, &Process::ProcessModel::loopsChanged, this, [&] { updateZoomRatio(); });
6✔
131
  con(process, &Process::ProcessModel::loopDurationChanged, this,
12✔
132
      [&] { updateZoomRatio(); });
6✔
133
  con(process, &Process::ProcessModel::startOffsetChanged, this,
12✔
134
      [&] { updateZoomRatio(); });
6✔
135

136
  // Selection
137
  con(process.selection, &Selectable::changed, this, &NodeItem::setSelected);
6✔
138
  m_selected = process.selection.get();
6✔
139

140
  if(!m_folded)
6✔
141
    createContentItem();
6✔
142
  else
UNCOV
143
    createFoldedItem();
×
144

145
  // This is because if it's a DefaultEffectItem the ports are created asynchronously
146
  // Thus they are added to the vertical lists instead of the actual items
147
  ossia::qt::run_async(this, [this] {
10✔
148
    resetInlets();
4✔
149
    resetOutlets();
4✔
150

151
    resizeAsync();
4✔
152
  });
4✔
153
  connect(&process, &Process::ProcessModel::controlAdded, this, &NodeItem::resetInlets);
6✔
154
  connect(
6✔
155
      &process, &Process::ProcessModel::controlRemoved, this, &NodeItem::resetInlets);
6✔
156
  connect(
6✔
157
      &process, &Process::ProcessModel::controlOutletAdded, this,
6✔
158
      &NodeItem::resetOutlets);
159
  connect(
6✔
160
      &process, &Process::ProcessModel::controlOutletRemoved, this,
6✔
161
      &NodeItem::resetOutlets);
162
  connect(&process, &Process::ProcessModel::inletsChanged, this, &NodeItem::resetInlets);
6✔
163
  connect(
6✔
164
      &process, &Process::ProcessModel::outletsChanged, this, &NodeItem::resetOutlets);
6✔
165

166
  connect(m_fold, &score::QGraphicsPixmapToggle::toggled, this, [this](bool b) {
6✔
167
    resetItem();
×
168

169
    m_folded = !b;
×
170
    if(!m_folded)
×
171
    {
UNCOV
172
      createContentItem();
×
173
    }
×
174
    else
175
    {
176
      createFoldedItem();
×
177
    }
UNCOV
178
    QTimer::singleShot(1, this, [this] {
×
179
      resetInlets();
×
180
      resetOutlets();
×
181

UNCOV
182
      resizeAsync();
×
UNCOV
183
    });
×
UNCOV
184
  });
×
185

186
  resizeAsync();
6✔
187
  updateTooltip();
6✔
188
}
6✔
189

UNCOV
190
void NodeItem::createWithoutDecorations()
×
191
{
192
  auto& process = m_model;
×
193

194
  con(process.selection, &Selectable::changed, this, &NodeItem::setSelected);
×
UNCOV
195
  m_selected = process.selection.get();
×
196

197
  createContentItem();
×
198

UNCOV
199
  updateTooltip();
×
UNCOV
200
}
×
201

202
void NodeItem::updateTooltip()
6✔
203
{
204
  if(!m_fx || m_fx->toolTip().isEmpty())
6✔
205
  {
206
    auto& p = this->m_context.app.interfaces<Process::ProcessFactoryList>();
6✔
207
    const auto& desc = p.get(m_model.concreteKey())->descriptor(m_model);
6✔
208

209
    bool has_name = !desc.prettyName.isEmpty();
6✔
210
    bool has_desc = !desc.description.isEmpty();
6✔
211
    bool has_link = !desc.documentationLink.isEmpty();
6✔
212
    QString str;
6✔
213
    if(has_name)
6✔
214
    {
215
      str += desc.prettyName;
6✔
216
      str += "\n";
6✔
217
    }
6✔
218
    if(has_desc)
6✔
219
    {
220
      str += desc.description;
6✔
221
      str += "\n";
6✔
222
    }
6✔
223
    if(has_link)
6✔
224
    {
225
      str += QStringLiteral("Press F1 to open the docs:\n%1")
12✔
226
                 .arg(desc.documentationLink.toString());
6✔
227
    }
6✔
228
    else
229
    {
UNCOV
230
      str += QStringLiteral("Press F1 to open the docs!");
×
231
    }
232

233
    this->setToolTip(str);
6✔
234
  }
6✔
235
  else if(m_fx)
×
236
  {
UNCOV
237
    this->setToolTip(m_fx->toolTip());
×
238
  }
×
239
}
6✔
240

241
void NodeItem::resetItem()
×
242
{
243
  qDeleteAll(m_inlets);
×
244
  m_inlets.clear();
×
245

246
  qDeleteAll(m_outlets);
×
UNCOV
247
  m_outlets.clear();
×
248

249
  if(m_presenter)
×
250
  {
251
    QPointer<Process::LayerView> oldView = static_cast<Process::LayerView*>(m_fx);
×
252
    delete m_presenter;
×
253
    m_presenter = nullptr;
×
254
    if(oldView)
×
255
      delete oldView;
×
UNCOV
256
    m_fx = nullptr;
×
UNCOV
257
    QObject::disconnect(&m_model, &Process::ProcessModel::setSize, this, nullptr);
×
258
  }
×
259
  else
260
  {
UNCOV
261
    delete m_fx;
×
262
    m_fx = nullptr;
×
263
  }
264

265
  m_contentSize = QSizeF{minimalContentWidth(), minimalContentHeight()};
×
UNCOV
266
}
×
267

268
void NodeItem::resetInlets()
4✔
269
{
270
  // if(!m_presenter)
271
  //   return;
272
  qDeleteAll(m_inlets);
4✔
273
  m_inlets.clear();
4✔
274

275
  // We don't display ports on the left & right side
276
  // for normal items which are supposed to handle it
277
  if(dynamic_cast<Process::DefaultEffectItem*>(m_fx) && !m_presenter)
4✔
278
    return updateTitlePos();
×
279

280
  const qreal x = InletX0;
4✔
281
  qreal y = m_label ? InletY0 : InletY0 - 10.;
4✔
282
  auto& portFactory = score::GUIAppContext().interfaces<Process::PortFactoryList>();
4✔
283
  for(Process::Inlet* port : m_model.inlets())
5✔
284
  {
285
    if(port->displayHandledExplicitly)
1✔
286
      continue;
×
287

288
    Process::PortFactory* fact = portFactory.get(port->concreteKey());
1✔
289
    Dataflow::PortItem* item = fact->makePortItem(*port, m_context, this, this);
1✔
290
    item->setPos(x, y);
1✔
291
    item->setZValue(10);
1✔
292
    m_inlets.push_back(item);
1✔
293

294
    y += PortSpacing;
1✔
295
  }
296

297
  updateTitlePos();
4✔
298
}
4✔
299

300
void NodeItem::resetOutlets()
4✔
301
{
302
  // if(!m_presenter)
303
  //   return;
304
  qDeleteAll(m_outlets);
4✔
305
  m_outlets.clear();
4✔
306

307
  // We don't display ports on the left & right side
308
  // for normal items which are supposed to handle it
309
  if(dynamic_cast<Process::DefaultEffectItem*>(m_fx) && !m_presenter)
4✔
310
    return updateTitlePos();
×
311

312
  const qreal x = m_contentSize.width() + OutletX0;
4✔
313
  qreal y = m_label ? OutletY0 : OutletY0 - 10.;
4✔
314

315
  auto& portFactory = score::AppContext().interfaces<Process::PortFactoryList>();
4✔
316
  for(Process::Outlet* port : m_model.outlets())
8✔
317
  {
318
    if(port->displayHandledExplicitly)
4✔
319
      continue;
×
320
    Process::PortFactory* fact = portFactory.get(port->concreteKey());
4✔
321
    auto item = fact->makePortItem(*port, m_context, this, this);
4✔
322
    item->setPos(x, y);
4✔
323
    item->setZValue(10);
4✔
324
    m_outlets.push_back(item);
4✔
325

326
    y += PortSpacing;
4✔
327
  }
328
}
4✔
329

330
void NodeItem::updateLabel()
8✔
331
{
332
  if(const auto& name = m_model.metadata().getName(); !name.isEmpty())
8✔
333
  {
334
    m_label->setText(name);
8✔
335
  }
8✔
336
  else if(const auto& label = m_model.metadata().getLabel(); !label.isEmpty())
×
337
  {
UNCOV
338
    m_label->setText(label);
×
339
  }
×
340
  else
341
  {
UNCOV
342
    m_label->setText(m_model.prettyShortName());
×
343
  }
344

345
  if(auto resizeable = dynamic_cast<score::ResizeableItem*>(m_fx))
8✔
346
  {
UNCOV
347
    resizeable->setMinimumWidth(minimalContentWidth());
×
UNCOV
348
  }
×
349
  QTimer::singleShot(2, this, &NodeItem::updateSize);
8✔
350
}
8✔
351

352
void NodeItem::updateTitlePos()
38✔
353
{
354
  if(!m_label)
38✔
UNCOV
355
    return;
×
356

357
  double x0 = FoldX0;
38✔
358
  if(!m_inlets.empty())
38✔
359
    x0 += InletX0;
4✔
360

361
  m_fold->setPos({x0, FoldY0});
38✔
362
  x0 += UiX0 - FoldX0 + 2.;
38✔
363

364
  if(m_scriptButton)
38✔
365
  {
366
    m_scriptButton->setPos({x0, UiY0});
2✔
367
    x0 += UiX0 + SpacingIcon;
2✔
368
  }
2✔
369

370
  if(m_uiButton)
38✔
371
  {
UNCOV
372
    m_uiButton->setPos({x0, UiY0});
×
UNCOV
373
    x0 += UiX0 + SpacingIcon;
×
UNCOV
374
  }
×
375

376
  if(m_presetButton)
38✔
377
  {
378
    m_presetButton->setPos({x0, UiY0});
38✔
379
    x0 += UiX0 + SpacingIcon * 2.;
38✔
380
  }
38✔
381

382
  m_label->setPos({x0, TitleY0});
38✔
383
}
38✔
384

UNCOV
385
QSizeF NodeItem::size() const noexcept
×
386
{
387
  return m_contentSize;
×
388
}
389

390
void NodeItem::setSelected(bool s)
1✔
391
{
392
  m_selected = s;
1✔
393
  if(s)
1✔
394
    setFocus();
1✔
395
  else
396
    clearFocus();
×
397

398
  update();
1✔
399
}
1✔
400

401
QRectF NodeItem::contentRect() const noexcept
18✔
402
{
403
  if(!m_label)
18✔
404
  {
UNCOV
405
    return {
×
UNCOV
406
        0, 0, m_contentSize.width() + RightSideWidth,
×
UNCOV
407
        m_contentSize.height() + FooterHeight};
×
408
  }
409
  else
410
  {
411
    double x = 0;
18✔
412
    double y = -TitleHeight;
18✔
413
    double w = m_contentSize.width();
18✔
414
    const double h = m_contentSize.height() + TitleHeight + FooterHeight;
18✔
415
    if(!m_inlets.empty())
18✔
416
    {
417
      x -= LeftSideWidth;
2✔
418
      w += LeftSideWidth;
2✔
419
    }
2✔
420
    if(!m_outlets.empty() || m_presenter)
18✔
421
    { // FIXME make the redimension handle an item instead
422
      w += RightSideWidth;
18✔
423
    }
18✔
424
    return {x, y, w, h};
18✔
425
  }
426
}
18✔
427

428
void NodeItem::updateContentRect()
18✔
429
{
430
  m_contentRect = contentRect().adjusted(-2., -2., 2., 2.);
18✔
431
}
18✔
432

433
QRectF NodeItem::boundingRect() const
15✔
434
{
435
  return m_contentRect.adjusted(-2., -2., 2., 2.);
15✔
436
}
437

438
void NodeItem::createFoldedItem()
×
439
{
440
  if(m_fx)
×
441
    return;
×
442

443
  auto& ctx = m_context;
×
UNCOV
444
  auto& model = m_model;
×
445
  // Body
446
  score::ResizeableItem* resizeable{};
×
447
  {
448
    auto fx = new Process::DefaultEffectItem{true, model, ctx, this};
×
UNCOV
449
    m_fx = fx;
×
450
    m_contentSize = m_fx->boundingRect().size(); // FIXME async, size is not set yet
×
451
    resizeable = fx;
×
452
  }
UNCOV
453
  setupItem(resizeable);
×
UNCOV
454
}
×
455

456
void NodeItem::createContentItem()
6✔
457
{
458
  if(m_fx)
6✔
UNCOV
459
    return;
×
460

461
  auto& ctx = m_context;
6✔
462
  auto& model = m_model;
6✔
463
  // Body
464
  auto& fact = ctx.app.interfaces<Process::LayerFactoryList>();
6✔
465
  score::ResizeableItem* resizeable{};
6✔
466
  if(auto factory = fact.findDefaultFactory(model))
6✔
467
  {
468
    if(auto fx = factory->makeItem(model, ctx, this))
6✔
469
    {
470
      m_fx = fx;
2✔
471
      m_contentSize = m_fx->boundingRect().size(); // FIXME async, size is not set yet
2✔
472
      resizeable = fx;
2✔
473
    }
2✔
474
    else if(auto fx = factory->makeLayerView(model, ctx, this))
4✔
475
    {
476
      m_fx = fx;
4✔
477
      m_presenter = factory->makeLayerPresenter(model, fx, ctx, this);
4✔
478
      m_contentSize = m_model.size();
4✔
479
      m_presenter->setWidth(m_contentSize.width(), m_contentSize.width());
4✔
480
      m_presenter->setHeight(m_contentSize.height());
4✔
481

482
      // TODO review the resizing heuristic...
483
      if(m_presenter)
4✔
484
      {
485
        ::bind(model, Process::ProcessModel::p_size{}, this, [this](QSizeF s) {
9✔
486
          if(s != m_contentSize)
5✔
487
            setSize(s);
×
488
        });
5✔
489
          connect(m_presenter, &Process::LayerPresenter::contextMenuRequested, this, [this] (QPoint pos, QPointF sp) {
4✔
490
              auto menu = new QMenu;
×
UNCOV
491
              auto& reg = score::GUIAppContext()
×
492
                              .applicationPlugin<Process::ApplicationPlugin>()
×
493
                              .layerContextMenuRegistrar();
×
494

495
              m_presenter->fillContextMenu(*menu, pos, sp, reg);
×
496
              if(menu->actions().size() > 0) {
×
UNCOV
497
                menu->exec(pos);
×
498
                menu->close();
×
499
              }
×
500

501
              menu->deleteLater();
×
UNCOV
502
          }, Qt::QueuedConnection);
×
503
      }
4✔
504
    }
4✔
505
  }
6✔
506

507
  if(!m_fx)
6✔
508
  {
509
    auto fx = new Process::DefaultEffectItem{false, model, ctx, this};
×
510
    m_fx = fx;
×
UNCOV
511
    m_contentSize = m_fx->boundingRect().size(); // FIXME async, size is not set yet
×
UNCOV
512
    resizeable = fx;
×
UNCOV
513
  }
×
514

515
  setupItem(resizeable);
6✔
516
}
6✔
517

518
void NodeItem::resizeAsync()
16✔
519
{
520
  updateTitlePos();
16✔
521
  if(auto resizeable = dynamic_cast<score::ResizeableItem*>(m_fx))
16✔
522
  {
523
    resizeable->setMinimumWidth(minimalContentWidth());
4✔
524
    // Needed to leave some time for defaulteffectitem
525
    QTimer::singleShot(2, this, [this] {
4✔
UNCOV
526
      updateSize();
×
UNCOV
527
      updateZoomRatio();
×
UNCOV
528
    });
×
529
  }
4✔
530
  else
531
  {
532
    QMetaObject::invokeMethod(this, [this] {
24✔
533
      updateSize();
12✔
534
      updateZoomRatio();
12✔
535
    });
12✔
536
  }
537
}
16✔
538

539
void NodeItem::recreate()
×
540
{
541
  resetItem();
×
542
  qDeleteAll(this->childItems());
×
543
  m_scriptButton = nullptr;
×
544
  m_uiButton = nullptr;
×
545
  m_presetButton = nullptr;
×
546
  m_label = nullptr;
×
547
  m_fold = nullptr;
×
UNCOV
548
  disconnect(&this->m_model, nullptr, this, nullptr);
×
549
  disconnect(&this->m_model.selection, nullptr, this, nullptr);
×
UNCOV
550
  disconnect(&this->m_model.metadata(), nullptr, this, nullptr);
×
551

552
  if(this->m_model.flags() & Process::ProcessFlags::FullyCustomItem)
×
553
  {
UNCOV
554
    createWithoutDecorations();
×
555
  }
×
556
  else
557
  {
558
    createWithDecorations();
×
559
  }
560

UNCOV
561
  initConnections();
×
UNCOV
562
}
×
563

564
void NodeItem::setupItem(score::ResizeableItem* resizeable)
6✔
565
{
566
  // Positions / size
567
  double w = std::max(minimalContentWidth(), m_contentSize.width());
6✔
568
  double h = std::max(minimalContentHeight(), m_contentSize.height());
6✔
569
  m_contentSize = QSizeF{w, h};
6✔
570

571
  resizeAsync();
6✔
572
  publishSize();
6✔
573

574
  if(resizeable)
6✔
575
  {
576
    if(auto lay = dynamic_cast<score::GraphicsLayout*>(resizeable))
2✔
577
      connect(lay, &score::ResizeableItem::childrenSizeChanged, this, [this, lay] {
×
578
        lay->layout();
×
UNCOV
579
        lay->fitChildrenRect();
×
580
        m_contentSize = lay->rect().size();
×
581

UNCOV
582
        resizeAsync();
×
583
      });
×
584
    connect(resizeable, &score::ResizeableItem::sizeChanged, this, [this](QSizeF sz) {
2✔
585
      double w = std::max(minimalContentWidth(), sz.width());
×
UNCOV
586
      double h = std::max(minimalContentHeight(), sz.height());
×
587
      m_contentSize = QSizeF{w, h};
×
588

UNCOV
589
      resizeAsync();
×
UNCOV
590
    });
×
591
  }
2✔
592
}
6✔
593

594
double NodeItem::minimalContentWidth() const noexcept
28✔
595
{
596
  if(Q_UNLIKELY(!m_label))
28✔
597
  {
UNCOV
598
    if((m_inlets.size() + m_outlets.size()) <= 1)
×
599
      return 10.;
×
600
    else
UNCOV
601
      return 20.;
×
602
  }
603
  else
604
  {
605
    return std::max(75.0, m_label->pos().x() + m_label->boundingRect().width() + 6.);
28✔
606
  }
607
}
28✔
608
double NodeItem::minimalContentHeight() const noexcept
24✔
609
{
610
  if(Q_UNLIKELY(!m_label))
24✔
UNCOV
611
    if(std::max(m_inlets.size(), m_outlets.size()) <= 1)
×
UNCOV
612
      return 10.;
×
613

614
  double h = 22.;
24✔
615
  if(!m_inlets.empty())
24✔
616
    h = std::max(h, m_inlets.back()->y() + 10.);
2✔
617
  if(!m_outlets.empty())
24✔
618
    h = std::max(h, m_outlets.back()->y() + 10.);
10✔
619
  return h;
24✔
620
}
24✔
621

622
void NodeItem::updateSize()
18✔
623
{
624
  if(m_model.flags() & Process::ProcessFlags::FullyCustomItem)
18✔
625
  {
626
    if(m_presenter)
×
627
    {
628
      m_presenter->setWidth(m_contentSize.width(), m_contentSize.width());
×
629
      m_presenter->setHeight(m_contentSize.height());
×
630
    }
×
UNCOV
631
    QSizeF sz{10.0, 10.0};
×
632
    if(m_fx)
×
633
    {
634
      auto fx_sz = m_fx->boundingRect().size();
×
635
      sz.rwidth() = std::max(sz.width(), fx_sz.width());
×
636
      sz.rheight() = std::max(sz.height(), fx_sz.height());
×
637
    }
×
638
    updateContentRect();
×
639
    update();
×
640
    if(auto sc = this->scene())
×
UNCOV
641
      sc->update();
×
UNCOV
642
    return;
×
643
  }
644

645
  if(!m_label)
18✔
646
  {
647
    if(auto sc = this->scene())
×
UNCOV
648
      sc->update();
×
649
    return;
×
650
  }
651
  QSizeF sz{minimalContentWidth(), minimalContentHeight()};
18✔
652
  if(m_fx)
18✔
653
  {
654
    auto fx_sz = m_fx->boundingRect().size();
18✔
655
    sz.rwidth() = std::max(sz.width(), fx_sz.width());
18✔
656
    sz.rheight() = std::max(sz.height(), fx_sz.height());
18✔
657
  }
18✔
658
  //if (sz != m_contentSize || !m_fx)
659
  {
660
    prepareGeometryChange();
18✔
661
    if(m_fx)
18✔
662
    {
663
      m_contentSize = sz;
18✔
664
      if(!m_presenter)
18✔
665
      {
UNCOV
666
        if(m_model.outlets().size() > 0 && m_model.inlets().size() == 0)
×
667
        {
668
          // Align right for the case where we only have a series of outlets
669
          updateContentRect();
×
UNCOV
670
          m_fx->setPos(m_contentRect.width() - m_fx->boundingRect().width(), 0);
×
UNCOV
671
        }
×
672
        else
673
        {
674
          m_fx->setPos(0, 0);
×
675
        }
UNCOV
676
      }
×
677
    }
18✔
678

679
    if(m_presenter)
18✔
680
    {
681
      m_presenter->setWidth(m_contentSize.width(), m_contentSize.width());
18✔
682
      m_presenter->setHeight(m_contentSize.height());
18✔
683
    }
18✔
684

685
    if(m_scriptButton)
18✔
686
    {
687
      m_scriptButton->setParentItem(nullptr);
×
UNCOV
688
    }
×
689
    if(m_uiButton)
18✔
690
    {
UNCOV
691
      m_uiButton->setParentItem(nullptr);
×
692
    }
×
693

694
    for(auto& outlet : m_outlets)
28✔
695
    {
696
      outlet->setPos(m_contentSize.width() + OutletX0, outlet->pos().y());
10✔
697
    }
698

699
    if(m_scriptButton)
18✔
700
    {
701
      m_scriptButton->setParentItem(this);
×
UNCOV
702
    }
×
703
    if(m_uiButton)
18✔
704
    {
705
      m_uiButton->setParentItem(this);
×
706
    }
×
707
    updateTitlePos();
18✔
708
    updateContentRect();
18✔
709
    publishSize();
18✔
710
    update();
18✔
711
    if(auto sc = this->scene())
18✔
712
      sc->update();
18✔
713
  }
714
}
18✔
715

716
void NodeItem::publishSize()
24✔
717
{
718
  // Node placement (e.g. creating a process after another one) reads the size
719
  // off the model: keep it in sync with what is actually drawn. A folded node
720
  // must not overwrite the unfolded size it will go back to.
721
  if(m_folded)
24✔
UNCOV
722
    return;
×
723
  if(m_model.flags() & Process::ProcessFlags::FullyCustomItem)
24✔
UNCOV
724
    return;
×
725
  if(m_model.size() == m_contentSize)
24✔
726
    return;
21✔
727

728
  const_cast<Process::ProcessModel&>(m_model).setSize(m_contentSize);
3✔
729
}
24✔
730

UNCOV
731
void NodeItem::setSize(QSizeF sz)
×
732
{
733
  if(m_presenter)
×
734
  {
735
    // TODO: find a way to indicate to a model what will be the size of the
736
    // item
737
    //      - maybe set it without a command in that case ?
738
    prepareGeometryChange();
×
739
    m_contentSize = sz;
×
740

741
    if(m_scriptButton)
×
742
    {
743
      m_scriptButton->setParentItem(nullptr);
×
744
    }
×
745
    if(m_uiButton)
×
746
    {
747
      m_uiButton->setParentItem(nullptr);
×
748
    }
×
749

750
    m_presenter->setWidth(sz.width(), sz.width());
×
751
    m_presenter->setHeight(sz.height());
×
UNCOV
752
    updateZoomRatio();
×
753

754
    resetInlets();
×
755
    resetOutlets();
×
756
    auto cur_x = m_contentSize.width() + TopButtonX0;
×
757
    if(m_scriptButton)
×
758
    {
759
      m_scriptButton->setParentItem(this);
×
UNCOV
760
      m_scriptButton->setPos({cur_x, TopButtonY0});
×
761
      cur_x += UiX0 + SpacingIcon;
×
UNCOV
762
    }
×
763
    if(m_uiButton)
×
764
    {
UNCOV
765
      m_uiButton->setParentItem(this);
×
UNCOV
766
      m_uiButton->setPos({cur_x, TopButtonY0});
×
UNCOV
767
    }
×
UNCOV
768
  }
×
UNCOV
769
  if(auto resizeable = dynamic_cast<score::ResizeableItem*>(m_fx))
×
770
  {
771
    resizeable->setMinimumWidth(sz.width());
×
UNCOV
772
  }
×
773
  updateContentRect();
×
UNCOV
774
  update();
×
775
  if(auto sc = this->scene())
×
776
    sc->update();
×
777
}
×
778

UNCOV
779
const Id<Process::ProcessModel>& NodeItem::id() const noexcept
×
780
{
UNCOV
781
  return m_model.id();
×
782
}
783

784
NodeItem::~NodeItem()
12✔
785
{
12✔
786
  delete m_presenter;
6✔
787
}
12✔
788

UNCOV
789
void NodeItem::setParentDuration(TimeVal r)
×
790
{
791
  if(r != m_parentDuration)
×
792
  {
793
    m_parentDuration = r;
×
UNCOV
794
    updateZoomRatio();
×
795
  }
×
796
}
×
797

798
void NodeItem::setPlayPercentage(float f, TimeVal parent_dur)
4✔
799
{
800
  if(this->m_model.flags() & Process::ProcessFlags::FullyCustomItem)
4✔
801
    return;
×
802

803
  if(m_playPercentage != f)
4✔
804
  {
805
    // Comes from the interval -> if we are looping we make a small modulo of it
806
    if(m_model.loops() && m_model.loopDuration().impl > 0)
×
807
    {
808
      auto loopDur = m_model.loopDuration().impl;
×
809
      double playdur = double(f) * parent_dur.impl;
×
UNCOV
810
      f = std::fmod(playdur, loopDur) / loopDur;
×
811
      if(m_playPercentage != f)
×
812
      {
813
        m_playPercentage = f;
×
UNCOV
814
        update({12., -1., (m_contentSize.width() - 24.), 3.});
×
UNCOV
815
      }
×
816
    }
×
817
    else
818
    {
UNCOV
819
      f = ossia::clamp(f, 0.f, 1.f);
×
UNCOV
820
      if(m_playPercentage != f)
×
821
      {
UNCOV
822
        m_playPercentage = f;
×
UNCOV
823
        update({12., -1., (m_contentSize.width() - 24.) * f + 1., 3.});
×
UNCOV
824
      }
×
825
    }
UNCOV
826
  }
×
827
}
4✔
828

UNCOV
829
qreal NodeItem::width() const noexcept
×
830
{
831
  return m_contentSize.width();
×
832
}
833

834
qreal NodeItem::height() const
×
835
{
836
  return TitleHeight + m_contentSize.height() + FooterHeight;
×
837
}
838

839
void NodeItem::initConnections()
6✔
840
{
841
  auto& process = this->model();
6✔
842
  ::bind(process, Process::ProcessModel::p_position{}, this, [this](QPointF p) {
12✔
843
    if(p != pos())
6✔
844
      setPos(p);
6✔
845
  });
6✔
846

847
  auto on_sizeChanged = [this] {
6✔
UNCOV
848
    m_needResize = true;
×
UNCOV
849
    QTimer::singleShot(1, this, [this] {
×
UNCOV
850
      if(!m_needResize)
×
UNCOV
851
        return;
×
852
      m_needResize = false;
×
UNCOV
853
      resizeAsync();
×
854
    });
×
855
  };
×
856
  connect(&process, &Process::ProcessModel::controlAdded, this, on_sizeChanged);
6✔
857
  connect(&process, &Process::ProcessModel::controlRemoved, this, on_sizeChanged);
6✔
858
  connect(&process, &Process::ProcessModel::controlOutletAdded, this, on_sizeChanged);
6✔
859
  connect(&process, &Process::ProcessModel::controlOutletRemoved, this, on_sizeChanged);
6✔
860
  connect(&process, &Process::ProcessModel::inletsChanged, this, on_sizeChanged);
6✔
861
  connect(&process, &Process::ProcessModel::outletsChanged, this, on_sizeChanged);
6✔
862
  connect(&process, &Process::ProcessModel::flagsChanged, this, &NodeItem::recreate);
6✔
863
}
6✔
864

865
const ProcessModel& NodeItem::model() const noexcept
19✔
866
{
867
  return m_model;
19✔
868
}
869

870
void NodeItem::updateZoomRatio() const noexcept
12✔
871
{
872
  const auto dur = m_model.loops() ? m_model.loopDuration() : m_parentDuration;
12✔
873
  if(m_presenter)
12✔
874
  {
875
    m_presenter->on_zoomRatioChanged(dur.impl / m_contentSize.width());
12✔
876
    // TODO investigate why this is necessary for scenario:
877
    m_presenter->parentGeometryChanged();
12✔
878
  }
12✔
879
}
12✔
880

881
bool NodeItem::isInSelectionCorner(QPointF p, QRectF r) const
×
882
{
883
  return (p.x() - r.x()) > (r.width() - 10.) && (p.y() - r.y()) > (r.height() - 10.);
×
884
}
885

886
void NodeItem::paint(
×
887
    QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
888
{
UNCOV
889
  auto& style = Process::Style::instance();
×
UNCOV
890
  const auto& skin = style.skin;
×
891
  const auto rect = m_contentRect;
×
892
  //painter->fillRect(boundingRect(), Qt::red);
893
  //return;
UNCOV
894
  const auto flags = this->m_model.flags();
×
895

UNCOV
896
  const auto& bset = skin.Emphasis5;
×
UNCOV
897
  const auto& fillbrush = !(flags & Process::ProcessFlags::NodeHasNoBackground)
×
UNCOV
898
                              ? skin.Emphasis5
×
UNCOV
899
                              : skin.TransparentBrush;
×
UNCOV
900
  const auto& brush = m_selected ? skin.Base2.darker
×
UNCOV
901
                      : m_hover  ? bset.lighter
×
UNCOV
902
                                 : bset.main;
×
903
  const auto& pen = m_dropping ? skin.Warn2.main.pen3_solid_round_round
×
UNCOV
904
                               : brush.pen2_solid_round_round;
×
905

906
  painter->setRenderHint(QPainter::Antialiasing, true);
×
907

908
  // Body
UNCOV
909
  painter->setPen(pen);
×
910

911
#if defined(SCORE_DEBUG_REDRAWS)
912
  {
913
    QColor c;
914
    c.setRedF(0.5 * double(rand()) / RAND_MAX);
915
    c.setGreenF(0.5 * double(rand()) / RAND_MAX);
916
    c.setBlueF(0.5 * double(rand()) / RAND_MAX);
917
    c.setAlphaF(1.);
918
    painter->setBrush(c);
919
  }
920
#else
UNCOV
921
  painter->setBrush(fillbrush);
×
922
#endif
923

924
  painter->drawRoundedRect(rect, Corner, Corner);
×
925

926
  painter->setRenderHint(QPainter::Antialiasing, false);
×
927

928
  // Exec
929
  if(!(flags & Process::ProcessFlags::FullyCustomItem))
×
930
    if(m_playPercentage > 0.)
×
931
    {
932
      painter->setPen(style.IntervalPlayFill().main.pen1_solid_flat_miter);
×
933
      painter->drawLine(
×
UNCOV
934
          QPointF{12., 0.}, QPointF{12. + (width() - 24.) * m_playPercentage, 0.});
×
UNCOV
935
    }
×
936

937
  // Resizing handle
UNCOV
938
  if(m_presenter)
×
939
  {
UNCOV
940
    const auto h = m_contentSize.height();
×
UNCOV
941
    const auto w = m_contentSize.width()
×
UNCOV
942
                   + ((!m_outlets.empty() || m_presenter) ? RightSideWidth : 0);
×
UNCOV
943
    painter->setPen(style.IntervalWarning().main.pen0_solid_round);
×
UNCOV
944
    double start_x = w - 6.;
×
UNCOV
945
    double start_y = h - 6.;
×
UNCOV
946
    double center_x = w - 2.;
×
UNCOV
947
    double center_y = h - 2.;
×
UNCOV
948
    painter->drawLine(start_x, center_y, center_x, center_y);
×
949
    painter->drawLine(center_x, start_y, center_x, center_y);
×
UNCOV
950
  }
×
951
}
×
952

953
namespace
954
{
955
enum Interaction
956
{
957
  None,
958
  Move,
959
  Resize
960
} nodeItemInteraction{};
961
QSizeF origNodeSize{};
962
bool nodeDidMove{};
963
bool nodeSelectOnRelease{};
964
bool nodeSelectCumulation{};
965
}
966

967
void NodeItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
×
968
{
UNCOV
969
  nodeItemInteraction = Interaction::None;
×
970
  if(event->button() == Qt::LeftButton)
×
971
  {
UNCOV
972
    nodeDidMove = false;
×
973
    if(m_presenter && isInSelectionCorner(event->pos(), m_contentRect))
×
974
    {
975
      nodeItemInteraction = Interaction::Resize;
×
UNCOV
976
      origNodeSize = m_model.size();
×
UNCOV
977
    }
×
978
    else
979
    {
980
      nodeItemInteraction = Interaction::Move;
×
981
    }
982

UNCOV
983
    if(m_presenter && m_model.flags() & Process::ProcessFlags::ItemRequiresUniqueFocus)
×
984
    {
985
      m_context.focusDispatcher.focus(m_presenter);
×
UNCOV
986
    }
×
987
    {
988
      const bool cumulation = event->modifiers() & Qt::ControlModifier;
×
UNCOV
989
      if(m_model.selection.get())
×
990
      {
UNCOV
991
        nodeSelectOnRelease = true;
×
UNCOV
992
        nodeSelectCumulation = cumulation;
×
UNCOV
993
      }
×
994
      else
995
      {
996
        nodeSelectOnRelease = false;
×
997
        Selection sel = filterSelections(
×
998
            &m_model, m_context.selectionStack.currentSelection(), cumulation);
×
999
        score::SelectionDispatcher{m_context.selectionStack}.select(sel);
×
1000
      }
×
1001
    }
1002
    event->accept();
×
1003
  }
×
1004
  else
1005
  {
UNCOV
1006
    return QGraphicsItem::mousePressEvent(event);
×
1007
  }
UNCOV
1008
}
×
1009
namespace
1010
{
1011
static std::vector<const Process::ProcessModel*>
1012
selectedProcesses(const score::DocumentContext& ctx)
×
1013
{
1014
  std::vector<const Process::ProcessModel*> ps;
×
1015
  auto sel = ctx.selectionStack.currentSelection();
×
1016
  ps.reserve(sel.size());
×
UNCOV
1017
  for(auto& obj : sel)
×
1018
    if(auto p = qobject_cast<Process::ProcessModel*>(obj))
×
1019
    {
1020
      ps.push_back(p);
×
UNCOV
1021
    }
×
UNCOV
1022
  return ps;
×
1023
}
×
1024
}
1025
void NodeItem::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
×
1026
{
1027
  if(nodeItemInteraction == Interaction::None)
×
UNCOV
1028
    return QGraphicsItem::mouseMoveEvent(event);
×
1029

1030
  auto parent = this->parentItem();
×
1031
  auto origp = parent->mapFromScene(event->buttonDownScenePos(Qt::LeftButton));
×
UNCOV
1032
  auto p = parent->mapFromScene(event->scenePos());
×
UNCOV
1033
  if(p != origp)
×
1034
    nodeDidMove = true;
×
1035

1036
  if(nodeDidMove)
×
1037
  {
1038
    switch(nodeItemInteraction)
×
1039
    {
1040
      case Interaction::Resize: {
UNCOV
1041
        const auto sz = origNodeSize + QSizeF{p.x() - origp.x(), p.y() - origp.y()};
×
1042
        m_dispatcher.submit<Process::ResizeNode>(
×
1043
            m_model, sz.expandedTo({minimalContentWidth(), minimalContentHeight()}));
×
UNCOV
1044
        updateSize();
×
1045
        break;
×
1046
      }
1047
      case Interaction::Move: {
UNCOV
1048
        m_dispatcher.submit<Process::MoveNodes>(selectedProcesses(m_context), p - origp);
×
1049
        break;
×
1050
      }
1051
      default:
UNCOV
1052
        break;
×
1053
    }
1054
  }
×
1055
  event->accept();
×
1056
}
×
1057

1058
void NodeItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
×
1059
{
1060
  if(nodeItemInteraction == Interaction::None)
×
UNCOV
1061
    return QGraphicsItem::mouseReleaseEvent(event);
×
1062

UNCOV
1063
  mouseMoveEvent(event);
×
1064

1065
  if(nodeDidMove)
×
1066
  {
1067
    m_dispatcher.commit<Process::MoveNodesMacro>();
×
UNCOV
1068
  }
×
1069
  else if(nodeSelectOnRelease)
×
1070
  {
1071
    Selection sel = filterSelections(
×
1072
        &m_model, m_context.selectionStack.currentSelection(), nodeSelectCumulation);
×
1073
    score::SelectionDispatcher{m_context.selectionStack}.select(sel);
×
UNCOV
1074
  }
×
UNCOV
1075
  nodeSelectOnRelease = false;
×
1076
  nodeDidMove = false;
×
UNCOV
1077
  event->accept();
×
UNCOV
1078
}
×
1079

1080
void NodeItem::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
×
1081
{
1082
  event->accept();
×
UNCOV
1083
}
×
1084

UNCOV
1085
void NodeItem::hoverEnterEvent(QGraphicsSceneHoverEvent* event)
×
1086
{
UNCOV
1087
  if(m_presenter && isInSelectionCorner(event->pos(), m_contentRect))
×
1088
  {
1089
    auto& skin = score::Skin::instance();
×
1090
    setCursor(skin.CursorScaleFDiag);
×
UNCOV
1091
  }
×
1092
  else
1093
  {
UNCOV
1094
    unsetCursor();
×
1095
  }
1096

UNCOV
1097
  m_hover = true;
×
1098
  update();
×
UNCOV
1099
  event->accept();
×
1100
}
×
1101

1102
void NodeItem::hoverMoveEvent(QGraphicsSceneHoverEvent* event)
×
1103
{
1104
  if(m_presenter && isInSelectionCorner(event->pos(), m_contentRect))
×
1105
  {
UNCOV
1106
    auto& skin = score::Skin::instance();
×
1107
    setCursor(skin.CursorScaleFDiag);
×
UNCOV
1108
  }
×
1109
  else
1110
  {
1111
    unsetCursor();
×
1112
  }
UNCOV
1113
  event->accept();
×
1114
}
×
1115

1116
void NodeItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
×
1117
{
1118
  unsetCursor();
×
1119

UNCOV
1120
  m_hover = false;
×
1121
  update();
×
UNCOV
1122
  event->accept();
×
1123
}
×
1124

1125
void NodeItem::keyPressEvent(QKeyEvent* event)
×
1126
{
1127
  if(event->key() == Qt::Key_Down)
×
1128
  {
1129
    auto ports = Dataflow::getProcessPorts(this->m_model);
×
1130
    if(!ports.empty())
×
1131
    {
1132
      score::SelectionDispatcher disp{m_context.selectionStack};
×
UNCOV
1133
      disp.select(*ports.front());
×
1134
    }
×
1135
  }
×
1136
  event->accept();
×
1137
}
×
1138

1139
void NodeItem::keyReleaseEvent(QKeyEvent* event)
×
1140
{
1141
  event->accept();
×
1142
}
×
1143
void NodeItem::dragEnterEvent(QGraphicsSceneDragDropEvent* event)
×
1144
{
UNCOV
1145
  m_dropping = true;
×
1146
  event->accept();
×
UNCOV
1147
  update();
×
1148
}
×
1149

1150
void NodeItem::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
×
1151
{
1152
  m_dropping = true;
×
UNCOV
1153
  event->accept();
×
UNCOV
1154
  update();
×
UNCOV
1155
}
×
1156

UNCOV
1157
void NodeItem::dragLeaveEvent(QGraphicsSceneDragDropEvent* event)
×
1158
{
UNCOV
1159
  m_dropping = false;
×
UNCOV
1160
  event->accept();
×
UNCOV
1161
  update();
×
UNCOV
1162
}
×
1163

UNCOV
1164
void NodeItem::dropEvent(QGraphicsSceneDragDropEvent* event)
×
1165
{
UNCOV
1166
  m_dropping = false;
×
UNCOV
1167
  event->accept();
×
UNCOV
1168
  dropReceived(event->pos(), *event->mimeData());
×
UNCOV
1169
  update();
×
UNCOV
1170
}
×
1171
}
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