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

ossia / score / 35899133125

23 Sep 2026 05:57PM UTC coverage: 41.488% (+1.2%) from 40.337%
35899133125

push

github

jcelerier
3rdparty: follow the merged libossia

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

123236 of 297041 relevant lines covered (41.49%)

113626.05 hits per line

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

97.06
/src/plugins/score-lib-process/Control/DefaultEffectItem.cpp
1
#include "DefaultEffectItem.hpp"
2

3
#include <Process/Dataflow/PortFactory.hpp>
4
#include <Process/Dataflow/PortItem.hpp>
5
#include <Process/DocumentPlugin.hpp>
6
#include <Process/Process.hpp>
7
#include <Process/ProcessContext.hpp>
8
#include <Process/Style/ScenarioStyle.hpp>
9

10
#include <Control/Layout.hpp>
11
#include <Effect/EffectLayout.hpp>
12

13
#include <score/application/GUIApplicationContext.hpp>
14
#include <score/graphics/GraphicsItem.hpp>
15
#include <score/graphics/TextItem.hpp>
16
#include <score/graphics/layouts/GraphicsGridLayout.hpp>
17
#include <score/graphics/widgets/QGraphicsPixmapButton.hpp>
18
#include <score/model/Skin.hpp>
19
#include <score/tools/Bind.hpp>
20
#include <score/widgets/Pixmap.hpp>
21

22
#include <ossia/detail/ssize.hpp>
23

24
#include <ossia-qt/invoke.hpp>
25

26
#include <QGraphicsScene>
27
namespace Process
28
{
29
static const constexpr qreal PagerHeight = 13.;
30
static const constexpr qreal PagerSpacing = 3.;
31

32
//! Leading non-control inlets get a column of their own once there are enough
33
//! of them; below that they share the control grid.
34
static bool hasOwnInletColumn(int leadingInlets) noexcept
334✔
35
{
36
  return leadingInlets >= Process::ControlsPerColumn;
334✔
37
}
38

39
DefaultEffectItem::DefaultEffectItem(
317✔
40
    bool onlyUndisplayed, const Process::ProcessModel& effect,
41
    const Process::Context& doc, QGraphicsItem* root)
42
    : score::EmptyRectItem{root}
317✔
43
    , m_effect{effect}
44
    , m_ctx{doc}
45
    , m_onlyUndisplayed{onlyUndisplayed}
46
{
317✔
47
  QObject::connect(
317✔
48
      &effect, &Process::ProcessModel::controlAdded, this, &DefaultEffectItem::reset);
317✔
49

50
  QObject::connect(
317✔
51
      &effect, &Process::ProcessModel::controlRemoved, this, &DefaultEffectItem::reset);
317✔
52

53
  QObject::connect(
317✔
54
      &effect, &Process::ProcessModel::controlOutletAdded, this,
317✔
55
      &DefaultEffectItem::reset);
56

57
  QObject::connect(
317✔
58
      &effect, &Process::ProcessModel::controlOutletRemoved, this,
317✔
59
      &DefaultEffectItem::reset);
60

61
  reset();
317✔
62

63
  QObject::connect(
317✔
64
      &effect, &Process::ProcessModel::inletsChanged, this, &DefaultEffectItem::reset);
317✔
65
  QObject::connect(
317✔
66
      &effect, &Process::ProcessModel::outletsChanged, this, &DefaultEffectItem::reset);
317✔
67

68
  connect(
317✔
69
      this, &score::ResizeableItem::childrenSizeChanged, this,
70
      &DefaultEffectItem::relayout);
71
  connect(
317✔
72
      this, &score::ResizeableItem::minimumWidthChanged, this,
73
      &DefaultEffectItem::updateMinimumWidth);
74
}
×
75

76
DefaultEffectItem::~DefaultEffectItem() { }
625✔
77

78
// The port items register themselves by model port in the document's port map,
79
// and the model ports of a process with dynamic ports may be deleted right
80
// after inletsChanged(): they cannot wait for the deferred deletion of the
81
// layout below, or the next port at the same address would come up as a
82
// zombie. deleteGraphicsItem() still defers the one that is delivering an
83
// event (a cable being dragged from it), see GraphicsItem.hpp: the map entry
84
// is therefore handed over here rather than in ~PortItem, so that the item the
85
// rebuild creates for the same port owns it and is born visible.
86
static void deletePortItems(Process::DataflowManager& plug, QGraphicsItem* it)
633✔
87
{
88
  const auto items = it->childItems();
633✔
89
  for(auto ptr : items)
1,446✔
90
  {
91
    if(auto r = qgraphicsitem_cast<Dataflow::PortItem*>(ptr))
813✔
92
    {
93
      auto& ports = plug.ports();
205✔
94
      if(auto p = ports.find(&r->port()); p != ports.end() && p->second == r)
205✔
95
        ports.erase(p);
205✔
96
      deleteGraphicsItem(r);
205✔
97
    }
205✔
98
    else
99
      deletePortItems(plug, ptr);
608✔
100
  }
101
}
633✔
102

103
void DefaultEffectItem::reset()
450✔
104
{
105
  if(m_layout)
450✔
106
  {
107
    // The rest of the layout is only hidden here: reset() runs from inside
108
    // the edit of one of its own controls when that control resizes the
109
    // process's ports, and the control must survive the delivery of its event.
110
    // Hiding it also makes the scene release its mouse grab cleanly.
111
    deletePortItems(m_ctx.dataflow, m_layout);
25✔
112
    m_layout->setVisible(false);
25✔
113
    m_layout->deleteLater();
25✔
114
    m_layout = nullptr;
25✔
115
  }
25✔
116

117
  m_allLayouts.clear();
450✔
118

119
  delete m_pager;
450✔
120
  m_pager = nullptr;
450✔
121

122
  m_needRecreate = true;
450✔
123
  ossia::qt::run_async(this, &DefaultEffectItem::recreate);
450✔
124
}
450✔
125

126
DefaultEffectItem::PortsToDisplay DefaultEffectItem::computeDisplayedPorts() const
181✔
127
{
128
  PortsToDisplay res;
181✔
129

130
  // Folded, the node shows its routing: everything that is not a control, plus
131
  // the controls that take part in the patch.
132
  // Unfolded, the controls keep their declaration order and are walked through
133
  // one page at a time. A cable landing on a control that is not on the current
134
  // page is simply not drawn, as with the tabbed layouts.
135
  int controlCount = 0;
181✔
136
  int gridOffset = 0;
181✔
137
  if(!m_onlyUndisplayed)
181✔
138
  {
139
    for(Process::Inlet* e : m_effect.inlets())
1,712✔
140
      controlCount += Process::isControlPort(*e);
1,542✔
141
    for(Process::Outlet* e : m_effect.outlets())
408✔
142
      controlCount += Process::isControlPort(*e);
238✔
143

144
    // The leading non-control inlets are laid out in the same grid as the
145
    // controls unless there are enough of them for their own column: they take
146
    // up the first slots and shift where the columns end.
147
    int leading = 0;
170✔
148
    for(Process::Inlet* e : m_effect.inlets())
376✔
149
    {
150
      if(Process::isControlPort(*e))
344✔
151
        break;
138✔
152
      leading++;
206✔
153
    }
154
    if(!hasOwnInletColumn(leading))
170✔
155
      gridOffset = leading;
162✔
156
  }
170✔
157

158
  res.page = Process::nodeControlPage(controlCount, m_page, gridOffset);
181✔
159

160
  int controlIndex = 0;
181✔
161
  const auto take = [&](const Process::Port& p) {
2,589✔
162
    if(m_onlyUndisplayed)
2,408✔
163
      return Process::isVisibleWhenFolded(p);
628✔
164
    if(!Process::isControlPort(p))
1,780✔
165
      return true;
453✔
166
    const int idx = controlIndex++;
1,327✔
167
    return idx >= res.page.first && idx < res.page.last;
1,327✔
168
  };
2,408✔
169

170
  for(Process::Inlet* e : m_effect.inlets())
2,340✔
171
    if(take(*e))
2,159✔
172
      res.inlets.push_back(e);
1,130✔
173
  for(Process::Outlet* e : m_effect.outlets())
430✔
174
    if(take(*e))
249✔
175
      res.outlets.push_back(e);
249✔
176

177
  for(Process::Inlet* e : res.inlets)
397✔
178
  {
179
    if(Process::isControlPort(*e))
358✔
180
      break;
142✔
181
    res.leadingInlets++;
216✔
182
  }
183

184
  return res;
181✔
185
}
181✔
186

187
void DefaultEffectItem::updateDisplayedPorts()
177✔
188
{
189
  auto ports = computeDisplayedPorts();
177✔
190

191
  m_shownInlets = std::move(ports.inlets);
177✔
192
  m_shownOutlets = std::move(ports.outlets);
177✔
193
  m_leadingInlets = ports.leadingInlets;
177✔
194
  m_controlPage = ports.page;
177✔
195
  m_page = m_controlPage.page;
177✔
196

197
  if(m_onlyUndisplayed)
177✔
198
  {
199
    // Only the folded display depends on how the controls are wired up.
200
    const auto follow = [this](Process::Port& p) {
387✔
201
      if(!Process::isControlPort(p))
380✔
202
        return;
13✔
203
      QObject::connect(
367✔
204
          &p, &Process::Port::cablesChanged, this,
367✔
205
          &DefaultEffectItem::onPortWiringChanged, Qt::UniqueConnection);
206
      QObject::connect(
367✔
207
          &p, &Process::Port::addressChanged, this,
367✔
208
          &DefaultEffectItem::onPortWiringChanged, Qt::UniqueConnection);
209
    };
380✔
210
    for(Process::Inlet* e : m_effect.inlets())
380✔
211
      follow(*e);
373✔
212
    for(Process::Outlet* e : m_effect.outlets())
14✔
213
      follow(*e);
7✔
214
  }
7✔
215
}
177✔
216

217
void DefaultEffectItem::onPortWiringChanged()
4✔
218
{
219
  // Port::cablesChanged is emitted from inside the drop that creates the cable,
220
  // while the event is still being delivered to the port item: rebuilding here
221
  // would delete that very item under the caller's feet. Do it once the drop is
222
  // over, and only when the change actually shows.
223
  // One check per batch: loading a document wires up every port in a row.
224
  if(m_wiringCheckPending)
4✔
225
    return;
×
226
  m_wiringCheckPending = true;
4✔
227

228
  ossia::qt::run_async(this, [this] {
8✔
229
    m_wiringCheckPending = false;
4✔
230
    auto ports = computeDisplayedPorts();
4✔
231
    if(ports.inlets != m_shownInlets || ports.outlets != m_shownOutlets)
4✔
232
      reset();
4✔
233
  });
4✔
234
}
4✔
235

236
template <typename Port_T>
237
Process::ControlLayout
238
DefaultEffectItem::makeItem(Process::LayoutBuilderBase& b, Port_T& port)
1,369✔
239
{
240
  if(!m_onlyUndisplayed)
1,369✔
241
    return b.makePort(port);
1,354✔
242

243
  // Folded: the routing only, no control widget
244
  if(auto* f = b.portFactory.get(port.concreteKey()))
15✔
245
    return f->makeLabelItem(port, m_ctx, b.layout, this);
15✔
246
  return {};
×
247
}
248

249
void DefaultEffectItem::recreate()
197✔
250
{
251
  if(!m_needRecreate)
197✔
252
    return;
20✔
253
  m_needRecreate = false;
177✔
254

255
  updateDisplayedPorts();
177✔
256

257
  const bool has_i = !m_shownInlets.empty();
177✔
258
  const bool has_o = !m_shownOutlets.empty();
177✔
259

260
  if(has_i && has_o)
177✔
261
    recreate_both();
164✔
262
  else if(has_i)
13✔
263
  {
264
    // A long column of message inlets reads better next to the control grid
265
    // than merged into it.
266
    if(m_leadingInlets > 5)
8✔
267
      recreate_both();
×
268
    else
269
      recreate_onlyInlets();
8✔
270
  }
8✔
271
  else if(has_o)
5✔
272
    recreate_onlyOutlets();
5✔
273

274
  if(m_controlPage.paginated())
177✔
275
    createPager();
10✔
276

277
  updateRect();
177✔
278
}
197✔
279

280
void DefaultEffectItem::recreate_onlyInlets()
8✔
281
{
282
  auto& portFactory = m_ctx.app.interfaces<Process::PortFactoryList>();
8✔
283

284
  m_layout = new score::GraphicsDefaultLayout{this};
8✔
285
  m_allLayouts.push_back(m_layout);
8✔
286
  LayoutBuilderBase b{*this,       m_effect,          m_ctx,
40✔
287
                      portFactory, m_effect.inlets(), m_effect.outlets(),
24✔
288
                      m_layout,    {m_layout}};
8✔
289

290
  for(Process::Inlet* e : m_shownInlets)
16✔
291
  {
292
    SCORE_ASSERT(e->parent());
8✔
293
    auto item = makeItem(b, *e);
8✔
294
    SCORE_SOFT_ASSERT(item.container);
8✔
295
    if(item.container)
8✔
296
      item.container->setParentItem(m_layout);
8✔
297
    if(auto inlet = qobject_cast<Process::ControlInlet*>(e))
8✔
298
      con(*inlet, &Process::ControlInlet::domainChanged, this,
2✔
299
          &DefaultEffectItem::reset, Qt::UniqueConnection);
1✔
300
  }
301

302
  auto& lay = *m_layout;
8✔
303
  lay.layout();
8✔
304
  lay.fitChildrenRect();
8✔
305
}
8✔
306

307
void DefaultEffectItem::recreate_onlyOutlets()
5✔
308
{
309
  auto& portFactory = m_ctx.app.interfaces<Process::PortFactoryList>();
5✔
310

311
  auto outlet_layout = new score::GraphicsDefaultOutletLayout{this};
5✔
312
  // Against the right-hand edge, as GraphicsIORootLayout puts it when the node
313
  // also has inlets. Without this a node with outlets only -- a generator such
314
  // as the Random Characters shader, folded -- drew its texture outlet on the
315
  // left.
316
  outlet_layout->setMinimumWidth(m_minimumWidth);
5✔
317
  m_layout = outlet_layout;
5✔
318
  m_allLayouts.push_back(m_layout);
5✔
319
  LayoutBuilderBase b{*this,       m_effect,          m_ctx,
25✔
320
                      portFactory, m_effect.inlets(), m_effect.outlets(),
15✔
321
                      m_layout,    {m_layout}};
5✔
322

323
  for(Process::Outlet* e : m_shownOutlets)
10✔
324
  {
325
    SCORE_ASSERT(e->parent());
5✔
326
    auto item = makeItem(b, *e);
5✔
327
    SCORE_SOFT_ASSERT(item.container);
5✔
328
    if(item.container)
5✔
329
      item.container->setParentItem(m_layout);
5✔
330

331
    if(auto outlet = qobject_cast<Process::ControlOutlet*>(e))
5✔
332
      con(*outlet, &Process::ControlOutlet::domainChanged, this,
×
333
          &DefaultEffectItem::reset, Qt::UniqueConnection);
×
334
  }
335

336
  auto& lay = *m_layout;
5✔
337
  lay.layout();
5✔
338
  lay.fitChildrenRect();
5✔
339
}
5✔
340

341
void DefaultEffectItem::recreate_both()
164✔
342
{
343
  auto& portFactory = m_ctx.app.interfaces<Process::PortFactoryList>();
164✔
344

345
  auto layout = new score::GraphicsIORootLayout{this};
164✔
346
  m_allLayouts.push_back(layout);
164✔
347
  layout->setMinimumWidth(m_minimumWidth);
164✔
348
  m_layout = layout;
164✔
349

350
  LayoutBuilderBase b{*this,       m_effect,          m_ctx,
820✔
351
                      portFactory, m_effect.inlets(), m_effect.outlets(),
492✔
352
                      m_layout,    {m_layout}};
164✔
353

354
  const int split = hasOwnInletColumn(m_leadingInlets) ? m_leadingInlets : 0;
164✔
355

356
  if(split > 0)
164✔
357
  {
358
    auto inlet_layout = new score::GraphicsDefaultInletLayout{m_layout};
8✔
359
    m_allLayouts.push_back(inlet_layout);
8✔
360
    for(int i = 0; i < split; i++)
72✔
361
    {
362
      auto item = makeItem(b, *m_shownInlets[i]);
64✔
363
      SCORE_SOFT_ASSERT(item.container);
64✔
364
      if(item.container)
64✔
365
        item.container->setParentItem(inlet_layout);
64✔
366
    }
64✔
367
    inlet_layout->layout();
8✔
368
    inlet_layout->fitChildrenRect();
8✔
369
  }
8✔
370

371
  // Folding can leave nothing but the split-off column: no empty layout then,
372
  // GraphicsIORootLayout lays out by child count.
373
  if(std::ssize(m_shownInlets) > split)
164✔
374
  {
375
    auto control_layout = new score::GraphicsDefaultLayout{m_layout};
156✔
376
    m_allLayouts.push_back(control_layout);
156✔
377
    for(int i = split, n = std::ssize(m_shownInlets); i < n; i++)
1,208✔
378
    {
379
      auto* e = m_shownInlets[i];
1,052✔
380
      SCORE_ASSERT(e->parent());
1,052✔
381
      auto item = makeItem(b, *e);
1,052✔
382
      SCORE_SOFT_ASSERT(item.container);
1,052✔
383
      if(item.container)
1,052✔
384
        item.container->setParentItem(control_layout);
1,052✔
385
      if(auto inlet = qobject_cast<Process::ControlInlet*>(e))
1,052✔
386
        con(*inlet, &Process::ControlInlet::domainChanged, this,
1,804✔
387
            &DefaultEffectItem::reset, Qt::UniqueConnection);
902✔
388
    }
1,052✔
389
    control_layout->layout();
156✔
390
    control_layout->fitChildrenRect();
156✔
391
  }
156✔
392

393
  if(!m_shownOutlets.empty())
164✔
394
  {
395
    auto outlet_layout = new score::GraphicsDefaultOutletLayout{m_layout};
164✔
396
    m_allLayouts.push_back(outlet_layout);
164✔
397
    for(Process::Outlet* e : m_shownOutlets)
404✔
398
    {
399
      SCORE_ASSERT(e->parent());
240✔
400
      auto item = makeItem(b, *e);
240✔
401
      SCORE_SOFT_ASSERT(item.container);
240✔
402
      if(item.container)
240✔
403
        item.container->setParentItem(outlet_layout);
240✔
404

405
      if(auto outlet = qobject_cast<Process::ControlOutlet*>(e))
240✔
406
        con(*outlet, &Process::ControlOutlet::domainChanged, this,
×
407
            &DefaultEffectItem::reset, Qt::UniqueConnection);
×
408
    }
409
    outlet_layout->layout();
164✔
410
    outlet_layout->fitChildrenRect();
164✔
411
  }
164✔
412

413
  auto& lay = *m_layout;
164✔
414
  lay.layout();
164✔
415

416
  lay.setRect(lay.childrenBoundingRect());
164✔
417
  lay.fitChildrenRect();
164✔
418
}
164✔
419

420
void DefaultEffectItem::createPager()
10✔
421
{
422
  // Dimmed at rest, accented while pressed: the pager is chrome, the controls
423
  // above it are what the node is about.
424
  static const auto prev_on = score::get_pixmap(":/icons/arrow_left_on.png");
10✔
425
  static const auto prev_off = score::get_pixmap(":/icons/arrow_left_disabled.png");
10✔
426
  static const auto next_on = score::get_pixmap(":/icons/arrow_right_on.png");
10✔
427
  static const auto next_off = score::get_pixmap(":/icons/arrow_right_disabled.png");
10✔
428

429
  m_pager = new score::EmptyRectItem{this};
10✔
430

431
  auto prev = new score::QGraphicsPixmapButton{prev_on, prev_off, m_pager};
10✔
432
  auto next = new score::QGraphicsPixmapButton{next_on, next_off, m_pager};
10✔
433

434
  // Same type as the port and control labels around it
435
  auto label = new score::SimpleTextItem{Process::labelBrush().main, m_pager};
10✔
436
  label->setText(
20✔
437
      QStringLiteral("%1/%2").arg(m_controlPage.page + 1).arg(m_controlPage.pageCount));
10✔
438

439
  const auto lab_w = label->boundingRect().width();
10✔
440
  const auto arrow_w = prev->boundingRect().width();
10✔
441

442
  const auto center = [](qreal h) { return std::round((PagerHeight - h) / 2.); };
40✔
443
  prev->setPos(0., center(prev->boundingRect().height()));
10✔
444
  label->setPos(arrow_w + PagerSpacing, center(label->boundingRect().height()));
10✔
445
  next->setPos(
20✔
446
      arrow_w + 2. * PagerSpacing + lab_w, center(next->boundingRect().height()));
10✔
447

448
  m_pager->setRect(
20✔
449
      {0., 0., 2. * arrow_w + 2. * PagerSpacing + lab_w + 2., PagerHeight});
10✔
450

451
  // The click is delivered from the button's own event handler: changing page
452
  // deletes it, so it has to happen once the event is done being dispatched.
453
  connect(prev, &score::QGraphicsPixmapButton::clicked, this, [this] {
15✔
454
    ossia::qt::run_async(this, [this] { setPage(m_page - 1); });
10✔
455
  });
5✔
456
  connect(next, &score::QGraphicsPixmapButton::clicked, this, [this] {
17✔
457
    ossia::qt::run_async(this, [this] { setPage(m_page + 1); });
14✔
458
  });
7✔
459
}
10✔
460

461
void DefaultEffectItem::setPage(int page)
12✔
462
{
463
  page = std::clamp(page, 0, m_controlPage.pageCount - 1);
12✔
464
  if(page == m_page)
12✔
465
    return;
4✔
466

467
  m_page = page;
8✔
468
  reset();
8✔
469
}
12✔
470

471
void DefaultEffectItem::updateRect()
182✔
472
{
473
  QRectF r = m_layout ? m_layout->rect() : QRectF{};
182✔
474
  if(m_pager)
182✔
475
  {
476
    const auto pager_r = m_pager->rect();
10✔
477
    m_pager->setPos(0., r.height());
10✔
478
    r.setHeight(r.height() + pager_r.height());
10✔
479
    r.setWidth(std::max(r.width(), pager_r.width()));
10✔
480
  }
10✔
481
  this->setRect(r);
182✔
482
}
182✔
483

484
// How wide the node ended up is not a reason to rebuild the ports: only the
485
// column that hangs off the right-hand edge has to be placed again.
486
void DefaultEffectItem::updateMinimumWidth()
316✔
487
{
488
  if(auto* root = dynamic_cast<score::GraphicsIORootLayout*>(m_layout))
316✔
489
    root->setMinimumWidth(m_minimumWidth);
5✔
490
  else if(auto* outlets = dynamic_cast<score::GraphicsDefaultOutletLayout*>(m_layout))
311✔
491
    outlets->setMinimumWidth(m_minimumWidth);
×
492

493
  relayout();
316✔
494
}
316✔
495

496
void DefaultEffectItem::relayout()
316✔
497
{
498
  if(m_layout)
316✔
499
  {
500
    for(auto it = m_allLayouts.rbegin(); it != m_allLayouts.rend(); ++it)
20✔
501
    {
502
      auto& lay = **it;
15✔
503
      lay.layout();
15✔
504
      lay.fitChildrenRect();
15✔
505
    }
15✔
506

507
    updateRect();
5✔
508
  }
5✔
509
}
316✔
510
}
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