Loading...
Searching...
No Matches
score-plugin-avnd/Crousti/Layer.hpp
1#pragma once
2#include <Process/Commands/SetControlValue.hpp>
3#include <Process/Dataflow/WidgetInlets.hpp>
4#include <Process/LayerPresenter.hpp>
5#include <Process/LayerView.hpp>
6
7#include <Control/Layout.hpp>
8#include <Crousti/MessageBus.hpp>
9#include <Crousti/ControlViews.hpp>
10#include <Crousti/Painter.hpp>
11#include <Crousti/ProcessModel.hpp>
12
13#include <score/command/Dispatchers/CommandDispatcher.hpp>
14#include <score/graphics/layouts/GraphicsBoxLayout.hpp>
15#include <score/graphics/layouts/GraphicsGridLayout.hpp>
16#include <score/graphics/layouts/GraphicsSplitLayout.hpp>
17#include <score/graphics/layouts/GraphicsStripDetailLayout.hpp>
18#include <score/graphics/layouts/GraphicsTabLayout.hpp>
19#include <score/graphics/layouts/LayoutSelection.hpp>
20#include <score/graphics/widgets/QGraphicsLineEdit.hpp>
21#include <score/tools/File.hpp>
22
23#include <avnd/common/aggregates.hpp>
24#include <avnd/common/enum_reflection.hpp>
25
26#include <QFileInfo>
27#include <QMimeData>
28#include <QRegularExpression>
29#include <QUrl>
30#include <QFontMetricsF>
31#include <avnd/concepts/file_port.hpp>
32#include <avnd/concepts/layout.hpp>
33#include <avnd/concepts/midifile.hpp>
34#include <avnd/concepts/soundfile.hpp>
35#include <halp/layout.hpp>
36
37namespace oscr
38{
39template <typename Item>
41 = avnd::container_layout<Item> || avnd::hbox_layout<Item> || avnd::group_layout<Item>
42 || avnd::vbox_layout<Item> || avnd::split_layout<Item> || avnd::grid_layout<Item>
43 || avnd::tab_layout<Item> || avnd::section_layout<Item>
44 || avnd::table_layout<Item> || avnd::strip_detail_layout<Item>;
45
46template <typename Info>
48{
49};
50template <typename Info>
51 requires requires { sizeof(typename Info::ui::bus); }
52struct MessageBusUi<Info>
53{
54 typename Info::ui::bus bus;
55};
56
57template <typename Info, typename RootLayout>
59 : RootLayout
60 , MessageBusUi<Info>
61{
62 using RootLayout::RootLayout;
63 typename Info::ui ui;
66 QObject* uiConnections{};
67};
68
69template <typename Item>
70static auto createRecursiveLayout(QGraphicsItem* parent)
71{
72 if constexpr(avnd::container_layout<Item>)
73 {
74 return new score::GraphicsLayout{parent};
75 }
76 else if constexpr(avnd::hbox_layout<Item> || avnd::group_layout<Item>)
77 {
78 return new score::GraphicsHBoxLayout{parent};
79 }
80 else if constexpr(avnd::vbox_layout<Item>)
81 {
82 return new score::GraphicsVBoxLayout{parent};
83 }
84 else if constexpr(avnd::section_layout<Item>)
85 {
86 return new score::GraphicsSectionLayout{parent};
87 }
88 else if constexpr(avnd::table_layout<Item>)
89 {
90 return new score::GraphicsTableLayout{parent};
91 }
92 else if constexpr(avnd::strip_detail_layout<Item>)
93 {
94 return new score::GraphicsStripDetailLayout{parent};
95 }
96 else if constexpr(avnd::split_layout<Item>)
97 {
98 return new score::GraphicsSplitLayout{parent};
99 }
100 else if constexpr(avnd::grid_layout<Item>)
101 {
102 if constexpr(requires { Item::columns(); })
103 {
104 return new score::GraphicsGridColumnsLayout{parent};
105 }
106 else if constexpr(requires { Item::rows(); })
107 {
108 return new score::GraphicsGridRowsLayout{parent};
109 }
110 }
111 else if constexpr(avnd::tab_layout<Item>)
112 {
113 return new score::GraphicsTabLayout{parent};
114 }
115 else if constexpr(avnd::has_layout<Item>)
116 {
117 return new score::GraphicsLayout{parent};
118 }
119 else
120 {
121 // static_assert(Item::no_layout_provided);
122 return new score::GraphicsVBoxLayout{parent};
123 }
124}
125
126template <typename Item>
127static void setupRecursiveLayout(auto* new_l)
128{
129 if constexpr(avnd::grid_layout<Item>)
130 {
131 if constexpr(requires { Item::columns(); })
132 {
133 new_l->setColumns(Item::columns());
134 }
135 else if constexpr(requires { Item::rows(); })
136 {
137 new_l->setRows(Item::rows());
138 }
139 }
140 else if constexpr(avnd::section_layout<Item>)
141 {
142 // Defaults a plugin can override with halp_meta(padding / spacing)
143 new_l->setPadding(6.);
144 new_l->setSpacing(4.);
145 if constexpr(requires { Item::name(); })
146 {
147 const std::string_view name = Item::name();
148 new_l->setTitle(QString::fromUtf8(name.data(), qsizetype(name.size())));
149 }
150 }
151 else if constexpr(avnd::table_layout<Item>)
152 {
153 // Defaults a plugin can override with halp_meta(padding / spacing)
154 new_l->setPadding(4.);
155 new_l->setSpacing(6.);
156 if constexpr(requires { Item::name(); })
157 {
158 const std::string_view name = Item::name();
159 new_l->setTitle(QString::fromUtf8(name.data(), qsizetype(name.size())));
160 }
161 if constexpr(requires { Item::columns(); })
162 {
163 QStringList titles;
164 for(std::string_view t : Item::columns())
165 titles.push_back(QString::fromUtf8(t.data(), qsizetype(t.size())));
166 new_l->setColumnTitles(std::move(titles));
167 }
168 }
169 else if constexpr(avnd::tab_layout<Item>)
170 {
171 if constexpr(avnd::tag_hide_tabs<Item>)
172 new_l->setTabBarVisible(false);
173 [=]<typename... Ts>(avnd::typelist<Ts...> args) {
174 (new_l->addTab(Ts::name()), ...);
175 }(avnd::as_typelist<Item>{});
176 }
177}
178
179template <typename T>
181template <typename T, typename V>
182struct pmf_member_type<V T::*>
183{
184 using type = V;
185};
186
187template <typename T>
188using pmf_member_type_t = typename pmf_member_type<T>::type;
189
190template <typename T>
192{
193 const score::DocumentContext& ctx;
194 void operator()(const ossia::value& v, auto& dst) const
195 {
196 T p;
197 oscr::from_ossia_value(p, v, dst);
198 }
199};
200
201// File & folder ports: paths are stored in the document as templates
202// (<PROJECT>:..., <LIBRARY>:..., or document-relative); resolve them so the
203// ui items always receive a usable absolute path.
204template <typename T>
205 requires(
206 avnd::soundfile_port<T> || avnd::midifile_port<T> || avnd::file_port<T>
207 || requires { T::widget::folder; })
209{
210 const score::DocumentContext& ctx;
211 void operator()(const ossia::value& v, auto& dst) const
212 {
213 T p;
214 if(auto str = v.target<std::string>(); str && !str->empty())
215 oscr::from_ossia_value(
216 p,
217 ossia::value{
218 score::locateFilePath(QString::fromStdString(*str), ctx).toStdString()},
219 dst);
220 else
221 oscr::from_ossia_value(p, v, dst);
222 }
223};
224
225// The inverse of SetGUIValue: an item hands back the absolute path it was
226// given, and the document must keep storing a template, or the project stops
227// being relocatable. Non-file ports pass through untouched.
228template <typename T>
230{
231 const score::DocumentContext& ctx;
232 ossia::value operator()(ossia::value v) const { return v; }
233};
234
235template <typename T>
236 requires(
237 avnd::soundfile_port<T> || avnd::midifile_port<T> || avnd::file_port<T>
238 || requires { T::widget::folder; })
240{
241 const score::DocumentContext& ctx;
242 ossia::value operator()(ossia::value v) const
243 {
244 if(auto str = v.target<std::string>(); str && !str->empty())
245 return ossia::value{
246 score::relativizeFilePath(QString::fromStdString(*str), ctx).toStdString()};
247 return v;
248 }
249};
250
252template <typename V>
253static constexpr int entryIndex(V v) noexcept
254{
255 if constexpr(std::is_enum_v<V>)
256 {
257 constexpr auto values = avnd::enum_values<V>();
258 for(std::size_t i = 0; i < values.size(); ++i)
259 if(values[i] == v)
260 return int(i);
261 return -1;
262 }
263 else
264 {
265 return static_cast<int>(v);
266 }
267}
268
270template <typename Item>
271static Process::ControlPresentation controlPresentation()
272{
274 if constexpr(requires {
275 { Item::presentation } -> std::convertible_to<halp::look>;
276 })
277 {
278 constexpr halp::look look = Item::presentation;
279 if(look.label[0] != 0)
280 p.label = QString::fromUtf8(look.label);
281 p.labelVisible = !look.hide_label;
282 if(look.size == halp::control_size::compact)
283 p.size = score::ControlSize::Small;
284 else if(look.size == halp::control_size::large)
285 p.size = score::ControlSize::Large;
286 p.valueOnHover = look.value == halp::value_display::hover;
287 if(look.widget == halp::control_widget::knob)
289 else if(look.widget == halp::control_widget::combo)
291 }
292 return p;
293}
294
295template <typename Info>
297{
298 using inputs_type = typename avnd::input_introspection<Info>::type;
299 using outputs_type = typename avnd::output_introspection<Info>::type;
300 inputs_type temp_inputs{};
301 outputs_type temp_outputs{};
302
303 typename Info::ui* rootUi{};
305 QObject* connections{};
306
309
311 std::map<std::string, std::shared_ptr<score::LayoutSelection>, std::less<>> selections;
312 std::map<score::GraphicsTableLayout*, std::shared_ptr<score::LayoutSelection>>
313 tableSelections;
314
315 std::shared_ptr<score::LayoutSelection> selectionNamed(std::string_view name)
316 {
317 auto it = selections.find(name);
318 if(it == selections.end())
319 {
320 auto sel = std::make_shared<score::LayoutSelection>();
321 // Stored on the root item, which survives UI rebuilds
322 const QByteArray key = "layoutSelection:" + QByteArray{name.data(), qsizetype(name.size())};
323 sel->current = context.property(key.constData()).toInt();
324 sel->listeners.push_back(
325 [ctx = &context, key](int i) { ctx->setProperty(key.constData(), i); });
326 it = selections.emplace(std::string{name}, std::move(sel)).first;
327 }
328 return it->second;
329 }
330
334 template <typename Port_T, typename Item>
336 QGraphicsItem* parent, Port_T* port, const Process::ControlLayout& lay, Item& item)
337 {
338 if constexpr(requires { sizeof(Item::value); })
339 {
340 using avnd_port_type = pmf_member_type_t<decltype(item.model)>;
341 SetGUIValue<avnd_port_type>{doc}(port->value(), item.value);
342 // The context outlives a UI rebuild (e.g. a preset reloading the
343 // ports); the widgets the lambda updates do not.
344 QObject* const lifetime = connections ? connections : &context;
345 if constexpr(requires { rootUi->on_control_update(); })
346 {
347 QObject::connect(
348 port, &Port_T::valueChanged, lifetime,
349 [rui = rootUi, layout = this->layout, &item,
350 &ctx = static_cast<const score::DocumentContext&>(this->doc)](
351 const ossia::value& v) {
352 SetGUIValue<avnd_port_type>{ctx}(v, item.value);
353
354 rui->on_control_update();
355 layout->update();
356 });
357 }
358 else
359 {
360 QObject::connect(
361 port, &Port_T::valueChanged, lifetime,
362 [layout = this->layout, &item,
363 &ctx = static_cast<const score::DocumentContext&>(this->doc)](
364 const ossia::value& v) {
365 SetGUIValue<avnd_port_type>{ctx}(v, item.value);
366 layout->update();
367 });
368 }
369
370 if constexpr(requires { item.set = {}; })
371 {
372 item.set = [port, &ctx = static_cast<const score::DocumentContext&>(this->doc)](
373 const auto& val) {
374 port->setValue(GetGUIValue<avnd_port_type>{ctx}(oscr::to_ossia_value(val)));
375 };
376 }
377 }
378
379 if constexpr(requires { Item::dynamic_size; })
380 {
381 if(auto obj = dynamic_cast<score::ResizeableItem*>(parent))
382 {
383 if(auto edit = qgraphicsitem_cast<score::QGraphicsLineEdit*>(lay.control))
384 QObject::connect(
385 edit, &score::QGraphicsLineEdit::sizeChanged, obj,
386 &score::ResizeableItem::childrenSizeChanged);
387 }
388 }
389 if constexpr(requires { Item::width; })
390 {
391 if(auto edit = qgraphicsitem_cast<score::QGraphicsLineEdit*>(lay.control))
392 edit->setTextWidth(Item::width());
393 }
394 }
395
398 template <typename Item>
399 void setupCondition(score::GraphicsLayout& lay, auto... recursive_members)
400 {
401 using Condition = decltype(Item::condition());
402 const int index = avnd::index_in_struct(
403 temp_inputs, recursive_members..., Condition::condition_control);
404 auto& model = static_cast<const ProcessModel<Info>&>(this->proc);
405 for(auto* base : model.avnd_input_idx_to_model_ports(index))
406 {
407 auto* port = qobject_cast<Process::ControlInlet*>(base);
408 if(!port)
409 continue;
410
411 auto matches = [port](const ossia::value& v) {
412 int idx = -1;
413 if(auto* e = qobject_cast<Process::Enum*>(port))
414 idx = e->indexOfValue(v);
415 else if(auto* c = qobject_cast<Process::ComboBox*>(port))
416 idx = c->indexOfValue(v);
417 const bool is_choice = qobject_cast<Process::Enum*>(port)
418 || qobject_cast<Process::ComboBox*>(port);
419 for(auto expected : Condition::condition_values())
420 {
421 if(is_choice ? idx == entryIndex(expected)
422 : ossia::convert<double>(v) == static_cast<double>(expected))
423 return true;
424 }
425 return false;
426 };
427 auto apply = [&lay, matches](const ossia::value& v) {
428 const bool on = matches(v);
429 if constexpr(Condition::condition_hides)
430 {
431 lay.setVisible(on);
432 }
433 else
434 {
435 lay.setEnabled(on);
436 lay.setOpacity(on ? 1. : 0.35);
437 }
438 };
439 apply(port->value());
440 // Scoped to the item, rebuilt on inlet change
441 QObject::connect(port, &Process::ControlInlet::valueChanged, &lay, apply);
442 break;
443 }
444 }
445
446 template <typename Item>
447 Process::ControlPresentation presentationFor() const
448 {
449 auto p = controlPresentation<Item>();
450 if(tableDepth > 0 && !p.label)
451 p.labelVisible = false;
452 return p;
453 }
454
455 template <typename Item>
456 void createControl(Item& item, auto... member)
457 {
458 if constexpr(requires { ((inputs_type{}).*....*member); })
459 {
460 int index = avnd::index_in_struct(temp_inputs, member...);
461 auto& proc = static_cast<const ProcessModel<Info>&>(this->proc);
462 auto ports = proc.avnd_input_idx_to_model_ports(index);
463 for(auto p : ports)
464 {
465 auto [port, qitem] = makeInlet(p, presentationFor<Item>());
466 {
467 SCORE_ASSERT(port);
468 SCORE_ASSERT(qitem.container);
469 setupControl(this->layout, port, qitem, item);
470 setupItem(item, *qitem.container);
471 }
472 }
473 }
474 else if constexpr(requires { ((outputs_type{}).*....*member); })
475 {
476 int index = avnd::index_in_struct(temp_outputs, member...);
477 auto& proc = static_cast<const ProcessModel<Info>&>(this->proc);
478 auto ports = proc.avnd_output_idx_to_model_ports(index);
479 for(auto p : ports)
480 {
481 auto [port, qitem] = makeOutlet(p, presentationFor<Item>());
482 {
483 SCORE_ASSERT(port);
484 SCORE_ASSERT(qitem.container);
485 setupControl(this->layout, port, qitem, item);
486 setupItem(item, *qitem.container);
487 }
488 }
489 }
490 else
491 {
492 static_assert(sizeof...(member) < 0, "not_a_member_of_inputs_or_outputs");
493 }
494 }
495
496 template <typename Item, typename T>
497 void createWidget(Item& it, const T& member)
498 {
499 if constexpr(requires {
500 { member } -> std::convertible_to<std::string_view>;
501 })
502 {
503 auto res = makeLabel(member);
504 setupItem(it, *res);
505 }
506 else if constexpr(requires {
507 { member.text } -> std::convertible_to<std::string_view>;
508 })
509 {
510 auto res = makeLabel(member.text);
511 setupItem(it, *res);
512 }
513 else
514 {
515 createControl(it, member);
516 }
517 }
518
519 template <typename Item, typename... T>
520 requires(sizeof...(T) > 1)
521 void createWidget(Item& item, T... recursive_members)
522 {
523 createControl(item, recursive_members...);
524 }
525
528 {
530 for(auto* item = this->layout; item && !cell; item = item->parentItem())
531 cell = dynamic_cast<score::GraphicsStripCell*>(item);
532 return cell;
533 }
534
537 {
538 auto* cell = enclosingCell();
539 if(!cell)
540 return;
541
542 // "Sound files (*.wav *.flac)" -> wav, flac. None: any file.
543 QStringList extensions;
544 static const QRegularExpression pattern{QStringLiteral("\\*\\.([A-Za-z0-9]+)")};
545 for(auto it = pattern.globalMatch(port.filters()); it.hasNext();)
546 extensions.push_back(it.next().captured(1).toLower());
547
548 auto file_of = [extensions](const QMimeData& data) -> QString {
549 for(const QUrl& url : data.urls())
550 {
551 if(!url.isLocalFile())
552 continue;
553 const QString path = url.toLocalFile();
554 if(extensions.isEmpty()
555 || extensions.contains(QFileInfo{path}.suffix().toLower()))
556 return path;
557 }
558 return {};
559 };
560 cell->setDropHandler(
561 [file_of](const QMimeData& data) { return !file_of(data).isEmpty(); },
562 [file_of, &port, &doc = this->doc](const QMimeData& data) {
563 const QString path = file_of(data);
564 if(path.isEmpty())
565 return;
566 CommandDispatcher<>{doc.commandStack}.submit<Process::SetControlValue>(
567 port, ossia::value{score::relativizeFilePath(path, doc).toStdString()});
568 });
569 }
570
572 static QString displayText(
573 const Process::ControlInlet& port, const ossia::value& v,
574 const QFontMetricsF& metrics, qreal max_width = 72.)
575 {
576 if(auto* c = qobject_cast<const Process::ComboBox*>(&port))
577 {
578 const int i = c->indexOfValue(v);
579 if(i >= 0 && i < std::ssize(c->alternatives))
580 return c->alternatives[i].first;
581 }
582 else if(auto* e = qobject_cast<const Process::Enum*>(&port))
583 {
584 const int i = e->indexOfValue(v);
585 if(i >= 0 && i < std::ssize(e->values))
586 return e->values[i];
587 }
588 if(auto str = v.target<std::string>())
589 {
590 QString text = QString::fromStdString(*str);
591 // Files: base name only
592 if(text.contains(QLatin1Char('/')) || text.contains(QLatin1Char('\\')))
593 text = QFileInfo{text}.completeBaseName();
594 else if(text.isEmpty())
595 text = QStringLiteral("\u2014");
596 return metrics.elidedText(text, Qt::ElideRight, max_width);
597 }
598 return QString::number(ossia::convert<double>(v), 'g', 4);
599 }
600
601 template <typename Item>
602 void createDisplay(Item& item, auto... member)
603 {
604 if constexpr(requires { ((inputs_type{}).*....*member); })
605 {
606 const int index = avnd::index_in_struct(temp_inputs, member...);
607 auto& proc = static_cast<const ProcessModel<Info>&>(this->proc);
608 for(auto* p : proc.avnd_input_idx_to_model_ports(index))
609 {
610 auto* port = qobject_cast<Process::ControlInlet*>(p);
611 if(!port)
612 continue;
613 if(auto* file = qobject_cast<Process::FileChooserBase*>(port))
614 acceptFileDrops(*file);
615 if constexpr(
616 requires { Item::style; } && Item::style == halp::display_style::bar)
617 {
618 auto* bar = new oscr::ValueBarItem;
619 auto show = [bar, port](const ossia::value& v) {
620 bar->setValue(oscr::normalizedValue(*port, v));
621 };
622 show(port->value());
623 QObject::connect(port, &Process::ControlInlet::valueChanged, bar, show);
624 setupItem(item, *bar);
625 }
626 else if constexpr(
627 requires { Item::style; } && Item::style == halp::display_style::title)
628 {
629 // No item: shown as the cell's title
630 if(auto* cell = enclosingCell())
631 {
632 auto show = [cell, port, metrics = QFontMetricsF{score::Skin::instance().Medium8Pt}](
633 const ossia::value& v) {
634 const auto str = v.target<std::string>();
635 cell->setShownTitle(
636 str && !str->empty() ? displayText(*port, v, metrics, 1e6) : QString{});
637 };
638 show(port->value());
639 QObject::connect(port, &Process::ControlInlet::valueChanged, cell, show);
640 }
641 }
642 else
643 {
644 auto* text
645 = new score::SimpleTextItem{score::Skin::instance().Base4.main, nullptr};
646 auto show = [text, port, metrics = QFontMetricsF{score::Skin::instance().Medium8Pt}](
647 const ossia::value& v) {
648 text->setText(displayText(*port, v, metrics));
649 };
650 show(port->value());
651 QObject::connect(port, &Process::ControlInlet::valueChanged, text, show);
652 setupItem(item, *text);
653 }
654 break;
655 }
656 }
657 }
658
659 void addControlInlet(std::vector<Process::ControlInlet*>& ports, auto... member)
660 {
661 if constexpr(requires { ((inputs_type{}).*....*member); })
662 {
663 const int index = avnd::index_in_struct(temp_inputs, member...);
664 auto& proc = static_cast<const ProcessModel<Info>&>(this->proc);
665 for(auto* p : proc.avnd_input_idx_to_model_ports(index))
666 {
667 if(auto* port = qobject_cast<Process::ControlInlet*>(p))
668 {
669 ports.push_back(port);
670 return;
671 }
672 }
673 }
674 else
675 {
676 static_assert(sizeof...(member) < 0, "not_a_member_of_inputs");
677 }
678 }
679
680 template <typename Item>
681 void createCustomMultiControl(Item& item, auto... recursive_members)
682 {
683 std::vector<Process::ControlInlet*> ports;
684 std::apply(
685 [&](auto... models) { (addControlInlet(ports, recursive_members..., models), ...); },
686 Item::models());
687 auto* qitem = new oscr::CustomMultiControl<Item&>{item, std::move(ports), this->doc};
688 setupItem(item, *qitem);
689 }
690
691 template <typename Item>
692 void createCustom(Item& item)
693 {
694 static_assert(!requires { item.transaction; });
695 auto res = new oscr::CustomItem<Item&>{item};
696 setupItem(item, *res);
697 }
698
699 template <typename Item>
700 void createCustomControl(Item& item, auto... member)
701 {
702 if constexpr(requires { ((inputs_type{}).*....*member); })
703 {
704 int index = avnd::index_in_struct(temp_inputs, member...);
705
706 auto& proc = static_cast<const ProcessModel<Info>&>(this->proc);
707 auto ports = proc.avnd_input_idx_to_model_ports(index);
708 for(auto p : ports)
709 {
710 if(auto* port = qobject_cast<Process::ControlInlet*>(p))
711 {
712 auto qitem = new oscr::CustomControl<Item&>{item, *port, this->doc};
713 Process::ControlLayout lay{.container = qitem};
714 if(auto* f = portFactory.get(port->concreteKey()))
715 {
716 lay.port_item = f->makePortItem(*port, this->doc, this->layout, &context);
717 }
718 setupControl(this->layout, port, lay, item);
719 setupItem(item, *qitem);
720 }
721 }
722 }
723 else if constexpr(requires { ((outputs_type{}).*....*member); })
724 {
725 int index = avnd::index_in_struct(temp_outputs, member...);
726
727 auto& proc = static_cast<const ProcessModel<Info>&>(this->proc);
728 auto ports = proc.avnd_output_idx_to_model_ports(index);
729 for(auto p : ports)
730 {
731 if(auto* port = qobject_cast<Process::ControlOutlet*>(p))
732 {
733 auto qitem = new oscr::CustomControl<Item&>{item, *port, this->doc};
734 Process::ControlLayout lay{.container = qitem};
735 if(auto* f = portFactory.get(port->concreteKey()))
736 {
737 lay.port_item = f->makePortItem(*port, this->doc, this->layout, &context);
738 }
739 setupControl(this->layout, port, lay, item);
740 setupItem(item, *qitem);
741 }
742 }
743 }
744 else
745 {
746 static_assert(sizeof...(member) < 0, "not_a_member_of_inputs_or_outputs");
747 }
748 }
749
750 template <typename Item>
751 void subLayout(Item& item, score::GraphicsLayout* new_l, auto... recursive_members)
752 {
753 auto old_l = layout;
754 setupLayout(item, *new_l);
755 setupItem(item, *new_l);
756 layout = new_l;
757 createdLayouts.push_back(new_l);
758
759 // A named row of a table starts with its name
760 if(auto table = dynamic_cast<score::GraphicsTableLayout*>(old_l))
761 {
762 if constexpr(requires { Item::name(); })
763 {
764 makeLabel(Item::name())->setParentItem(new_l);
765 table->setRowTitles(true);
766 }
767 if(auto row = dynamic_cast<score::GraphicsSelectableRow*>(new_l))
768 {
769 auto it = tableSelections.find(table);
770 if(it != tableSelections.end())
771 {
772 // Index: number of rows before it
773 int index = -1;
774 for(auto* child : table->childItems())
775 if(dynamic_cast<score::GraphicsSelectableRow*>(child))
776 ++index;
777 auto sel = it->second;
778 row->onActivated = [sel, index] { sel->select(index); };
779 sel->listen([row, index](int i) { row->setSelected(i == index); });
780 }
781 }
782 }
783 if constexpr(avnd::table_layout<Item> && requires { Item::columns(); })
784 ++tableDepth;
785
786 if constexpr(requires { Item::condition(); })
787 setupCondition<Item>(*new_l, recursive_members...);
788
789 if constexpr(avnd::table_layout<Item>)
790 {
791 forEachMember(item, [](auto& row) {
792 static_assert(
793 avnd::hbox_layout<std::remove_cvref_t<decltype(row)>>,
794 "The members of a halp table are its rows: structs with "
795 "halp_meta(layout, halp::layouts::hbox)");
796 });
797 }
798
799 // Rows of a table naming a selection select their unit when pressed
800 if constexpr(avnd::table_layout<Item> && requires { Item::selection(); })
801 {
802 auto* table = static_cast<score::GraphicsTableLayout*>(new_l);
803 table->setRowsSelectable(true);
804 tableSelections[table] = selectionNamed(Item::selection());
805 }
806
807 if constexpr(avnd::tab_layout<Item> && requires { Item::model; })
808 {
809 auto* tabs = static_cast<score::GraphicsTabLayout*>(new_l);
810 const int index
811 = avnd::index_in_struct(temp_inputs, recursive_members..., Item::model);
812
813 // Optional: static constexpr auto when() { return std::array{values...}; }
814 // Default: value i shows page i.
815 std::vector<int> page_of;
816 {
817 int page = 0;
818 forEachMember(item, [&](auto& p) {
819 using page_type = std::remove_cvref_t<decltype(p)>;
820 if constexpr(requires { page_type::when(); })
821 {
822 for(auto v : page_type::when())
823 {
824 const int i = entryIndex(v);
825 if(i < 0)
826 continue;
827 if(i >= std::ssize(page_of))
828 page_of.resize(i + 1, -1);
829 page_of[i] = page;
830 }
831 }
832 ++page;
833 });
834 }
835 auto to_page = [page_of](int i) {
836 return (i >= 0 && i < std::ssize(page_of) && page_of[i] >= 0) ? page_of[i] : i;
837 };
838 auto to_value_index = [page_of](int page) {
839 for(int i = 0; i < std::ssize(page_of); i++)
840 if(page_of[i] == page)
841 return i;
842 return page;
843 };
844 // A root tab layout outlives a UI rebuild: its connections go with the build
845 auto& model = static_cast<const ProcessModel<Info>&>(this->proc);
846 for(auto* base : model.avnd_input_idx_to_model_ports(index))
847 {
848 if(auto* port = dynamic_cast<Process::Enum*>(base))
849 {
850 auto select = [tabs, port, to_page](const ossia::value& value) {
851 if(const int i = port->indexOfValue(value); i >= 0)
852 tabs->setCurrentIndex(to_page(i));
853 };
854 select(port->value());
855 QObject::connect(
856 port, &Process::ControlInlet::valueChanged,
857 connections ? connections : static_cast<QObject*>(tabs), select);
858 if constexpr(!avnd::tag_hide_tabs<Item>)
859 tabs->onCurrentIndexChanged
860 = [port, to_value_index, &doc = this->doc](int i) {
861 const ossia::value value = port->valueAtIndex(to_value_index(i));
862 if(value.valid() && port->value() != value)
863 CommandDispatcher<>{doc.commandStack}.submit<Process::SetControlValue>(
864 *port, value);
865 };
866 break;
867 }
868 if(auto* port = dynamic_cast<Process::ComboBox*>(base))
869 {
870 auto select = [tabs, port, to_page](const ossia::value& value) {
871 if(const int i = port->indexOfValue(value); i >= 0)
872 tabs->setCurrentIndex(to_page(i));
873 };
874 select(port->value());
875 QObject::connect(
876 port, &Process::ControlInlet::valueChanged,
877 connections ? connections : static_cast<QObject*>(tabs), select);
878 if constexpr(!avnd::tag_hide_tabs<Item>)
879 tabs->onCurrentIndexChanged
880 = [port, to_value_index, &doc = this->doc](int i) {
881 const ossia::value value = port->valueAtIndex(to_value_index(i));
882 if(value.valid() && port->value() != value)
883 CommandDispatcher<>{doc.commandStack}.submit<Process::SetControlValue>(
884 *port, value);
885 };
886 break;
887 }
888 }
889 }
890
891 if constexpr(avnd::strip_detail_layout<Item>)
892 {
893 auto& strip = *static_cast<score::GraphicsStripDetailLayout*>(new_l);
894 createdLayouts.push_back(&strip.strip());
895 // Each page: a strip cell with its summary, then the page
896 auto page = [&](auto& p) {
897 auto* cell = new score::GraphicsStripCell{nullptr};
898 using page_type = std::remove_cvref_t<decltype(p)>;
899 if constexpr(requires { page_type::name(); })
900 {
901 const std::string_view name = page_type::name();
902 cell->setTitle(QString::fromUtf8(name.data(), qsizetype(name.size())));
903 }
904 strip.addCell(cell);
905 createdLayouts.push_back(cell);
906 // A member, so that it lives as long as the UI
907 if constexpr(requires { p.summary; })
908 {
909 layout = cell;
910 walkMembers(p.summary, recursive_members..., p.group);
911 layout = new_l;
912 }
913 this->walkLayout(p, recursive_members...);
914 };
915 forEachMember(item, page);
916
917 // e.g. table rows choose the page
918 if constexpr(requires { Item::selection(); })
919 {
920 auto sel = selectionNamed(Item::selection());
921 sel->listen([&strip](int i) { strip.setCurrentIndex(i); });
922 strip.onCurrentIndexChanged = [sel](int i) { sel->select(i); };
923 if constexpr(avnd::tag_hide_tabs<Item>)
924 strip.setStripVisible(false);
925 }
926 }
927 else
928 {
929 walkMembers(item, recursive_members...);
930 }
931
932 if constexpr(avnd::table_layout<Item> && requires { Item::columns(); })
933 --tableDepth;
934 layout = old_l;
935 }
936
937 template <typename Item, typename F>
938 static void forEachMember(Item& item, F&& f)
939 {
940#if AVND_USE_BOOST_PFR
941 static constexpr int N = avnd::pfr::tuple_size_v<Item>;
942 auto t = avnd::pfr::detail::tie_as_tuple(item);
943 [&]<std::size_t... I>(std::index_sequence<I...>) {
944 using namespace std;
945 using namespace avnd::pfr;
946 (f(get<I>(t)), ...);
947 }(std::make_index_sequence<N>{});
948#else
949 auto&& [... members] = item;
950 (f(members), ...);
951#endif
952 }
953
954 template <typename Item>
955 void walkMembers(Item& item, auto... recursive_members)
956 {
957 forEachMember(item, [&](auto& m) { this->walkLayout(m, recursive_members...); });
958 }
959
960 template <typename Item>
961 auto initRecursiveLayout()
962 {
963 if constexpr(avnd::hbox_layout<Item>)
964 {
965 // The rows of a table driving a selection can be selected
966 if(auto table = dynamic_cast<score::GraphicsTableLayout*>(this->layout);
967 table && tableSelections.contains(table))
968 {
970 setupRecursiveLayout<Item>(row);
971 return row;
972 }
973 }
974 auto new_l = createRecursiveLayout<Item>(this->layout);
975 setupRecursiveLayout<Item>(new_l);
976 return new_l;
977 }
978
979 template <typename Item>
980 void walkLayout(Item& item, auto... recursive_members)
981 {
982 if constexpr(avnd::spacing_layout<Item>)
983 {
984 auto widg = new score::EmptyRectItem{layout};
985 // Static (halp_meta) or per-instance, like halp::spacing{.width, .height}
986 double w = 1., h = 1.;
987 if constexpr(requires { Item::width(); })
988 w = Item::width();
989 else if constexpr(requires { item.width; })
990 w = item.width;
991 if constexpr(requires { Item::height(); })
992 h = Item::height();
993 else if constexpr(requires { item.height; })
994 h = item.height;
995 widg->setRect({0, 0, w, h});
996 }
997 else if constexpr(recursive_container_layout<Item>)
998 {
999 subLayout(item, initRecursiveLayout<Item>(), recursive_members...);
1000 }
1001 else if constexpr(avnd::control_layout<Item> && requires { Item::display_only; })
1002 {
1003 // Read-only view of a value, e.g. in a strip cell
1004 createDisplay(item, recursive_members..., item.model);
1005 }
1006 else if constexpr(avnd::control_layout<Item>)
1007 {
1008 // Widget with some metadata.. FIXME
1009 // Auto-generated item for a control
1010 createWidget(item, recursive_members..., item.model);
1011 }
1012 else if constexpr(avnd::custom_multi_control_layout<Item>)
1013 {
1014 // Custom-drawn item bound to several controls
1015 createCustomMultiControl(item, recursive_members...);
1016 }
1017 else if constexpr(avnd::custom_control_layout<Item>)
1018 {
1019 // Widget with some metadata.. FIXME
1020 // Custom-drawn item for a control
1021 createCustomControl(item, recursive_members..., item.model);
1022 }
1023 else if constexpr(avnd::custom_layout<Item>)
1024 {
1025 // Widget with some metadata.. FIXME
1026 // This is just a cosmetic item without behaviour or control attached
1027 createCustom(item);
1028 }
1029 else if constexpr(avnd::recursive_group_layout<Item>)
1030 {
1031 walkLayout(item.ui, recursive_members..., item.group);
1032 }
1033 else if constexpr(avnd::dynamic_controls<Item>)
1034 {
1035 walkLayout(item.ui, recursive_members..., item.group);
1036 }
1037 else if constexpr(avnd::has_layout<Item>)
1038 {
1039 // Treat it like group
1040 subLayout(item, initRecursiveLayout<Item>(), recursive_members...);
1041 }
1042 else
1043 {
1044 // Normal widget, e.g. just a const char*
1045 createWidget(item, item);
1046 }
1047 }
1048};
1049
1050template <typename Info>
1052{
1053public:
1054 virtual ~LayerFactory() { }
1055
1056private:
1057 std::optional<double> recommendedHeight() const noexcept override
1058 {
1059 if constexpr(requires { (double)Info::layout::height(); })
1060 {
1061 return Info::layout::height();
1062 }
1063 return Process::LayerFactory::recommendedHeight();
1064 }
1065
1066 UuidKey<Process::ProcessModel> concreteKey() const noexcept override
1067 {
1069 }
1070
1071 bool matches(const UuidKey<Process::ProcessModel>& p) const override
1072 {
1074 }
1075
1076 Process::LayerView* makeLayerView(
1077 const Process::ProcessModel& proc, const Process::Context& context,
1078 QGraphicsItem* parent) const final override
1079 {
1080 return nullptr;
1081 }
1082
1083 Process::LayerPresenter* makeLayerPresenter(
1085 const Process::Context& context, QObject* parent) const final override
1086 {
1087 return nullptr;
1088 }
1089
1090 template <typename Item>
1091 static void init_bus(ProcessModel<Info>& proc, Item& item)
1092 {
1093 auto ptr = &item;
1094 if constexpr(avnd::has_gui_to_processor_bus<Info>)
1095 {
1096 // ui -> engine
1097 ptr->bus.send_message = MessageBusSender{proc.from_ui};
1098 }
1099
1100 if constexpr(avnd::has_processor_to_gui_bus<Info>)
1101 {
1102 // engine -> ui
1103 proc.to_ui = [ptr = QPointer{ptr}](QByteArray mess) {
1104 // FIXME this is not enough as the message may be sent from another thread?
1105 if(!ptr)
1106 return;
1107
1108 if constexpr(requires { ptr->bus.process_message(); })
1109 {
1110 ptr->bus.process_message();
1111 }
1112 else if constexpr(requires { ptr->bus.process_message(ptr->ui); })
1113 {
1114 ptr->bus.process_message(ptr->ui);
1115 }
1116 else if constexpr(requires { ptr->bus.process_message(ptr->ui, {}); })
1117 {
1118 std::decay_t<avnd::second_argument<&Info::ui::bus::process_message>> arg;
1119 MessageBusReader b{mess};
1120 b(arg);
1121 ptr->bus.process_message(ptr->ui, std::move(arg));
1122 }
1123 else
1124 {
1125 ptr->bus.process_message(ptr->ui, {});
1126 }
1127 };
1128 }
1129
1130 if_possible(ptr->bus.init(ptr->ui));
1131 }
1132
1133 auto makeItemImpl(ProcessModel<Info>& proc, QGraphicsItem* parent) const noexcept
1134 {
1135 using ui_type = typename Info::ui;
1136 using root_layout_type
1137 = std::remove_cvref_t<decltype(*createRecursiveLayout<ui_type>(nullptr))>;
1138
1139 auto new_l = new RootItem<Info, root_layout_type>{parent};
1140 setupRecursiveLayout<ui_type>(new_l);
1141 if constexpr(requires { sizeof(typename Info::ui::bus); })
1142 init_bus(proc, *new_l);
1143
1144 if constexpr(requires { new_l->ui.start(); })
1145 {
1146 QObject::connect(&proc, &Process::ProcessModel::startExecution, new_l, [new_l] {
1147 new_l->ui.start();
1148 });
1149 }
1150 if constexpr(requires { new_l->ui.stop(); })
1151 {
1152 QObject::connect(&proc, &Process::ProcessModel::stopExecution, new_l, [new_l] {
1153 new_l->ui.stop();
1154 });
1155 }
1156 if constexpr(requires { new_l->ui.reset(); })
1157 {
1158 QObject::connect(&proc, &Process::ProcessModel::resetExecution, new_l, [new_l] {
1159 new_l->ui.reset();
1160 });
1161 }
1162 return new_l;
1163 }
1164
1165 score::ResizeableItem* makeItem(
1166 const Process::ProcessModel& proc, const Process::Context& ctx,
1167 QGraphicsItem* parent) const final override
1168 {
1169 using namespace score;
1170 auto& process = static_cast<const ProcessModel<Info>&>(proc);
1171
1172 auto rootItem = makeItemImpl(const_cast<ProcessModel<Info>&>(process), parent);
1173
1174 auto recreate = [parent, &proc, &ctx, rootItem] {
1175 delete rootItem->uiConnections;
1176 rootItem->uiConnections = new QObject{rootItem};
1177 LayoutBuilder<Info> b{
1178 *rootItem, proc,
1179 ctx, ctx.app.interfaces<Process::PortFactoryList>(),
1180 proc.inlets(), proc.outlets(),
1181 };
1182 b.rootUi = &rootItem->ui;
1183 b.connections = rootItem->uiConnections;
1184 b.layout = parent;
1185 // avnd UIs declare their own spacing
1186 b.marginOnNestedLayouts = false;
1187
1188 b.subLayout(rootItem->ui, rootItem);
1189
1190 b.finalizeLayout(rootItem);
1191
1192 rootItem->fitChildrenRect();
1193
1194 if_possible(b.rootUi->on_control_update());
1195 };
1196
1197 // Delete the connections before the widgets: a widget losing focus while
1198 // being deleted still emits its edit.
1199 auto rebuild = [rootItem, recreate] {
1200 delete rootItem->uiConnections;
1201 rootItem->uiConnections = nullptr;
1202 auto cld = rootItem->childItems();
1203 for(auto item : cld)
1204 {
1205 delete item;
1206 }
1207 recreate();
1208 };
1209 QObject::connect(&proc, &Process::ProcessModel::inletsChanged, rootItem, rebuild);
1210 QObject::connect(&proc, &Process::ProcessModel::outletsChanged, rootItem, rebuild);
1211
1212 recreate();
1213 return rootItem;
1214 }
1215};
1216
1217}
The CommandDispatcher class.
Definition CommandDispatcher.hpp:13
Definition Port.hpp:242
Definition score-lib-process/Process/ProcessFactory.hpp:59
Definition LayerPresenter.hpp:34
Definition LayerView.hpp:21
Definition PortFactory.hpp:110
The Process class.
Definition score-lib-process/Process/Process.hpp:80
Definition SetControlValue.hpp:14
Definition UuidKey.hpp:345
Definition Painter.hpp:551
Definition Painter.hpp:365
halp::custom_multi_control. Values are normalized; a gesture is one undo step.
Definition ControlViews.hpp:90
Definition score-plugin-avnd/Crousti/Layer.hpp:1052
Definition score-plugin-avnd/Crousti/ProcessModel.hpp:88
halp::display_style::bar
Definition ControlViews.hpp:25
Definition RectItem.hpp:78
Definition GraphicsGridLayout.hpp:13
Definition GraphicsGridLayout.hpp:27
Definition GraphicsBoxLayout.hpp:13
Definition GraphicsLayout.hpp:8
Titled vbox padded on all sides. Sizes itself, independent of parent margins.
Definition GraphicsBoxLayout.hpp:56
Table row selected by a press on its background or controls.
Definition GraphicsBoxLayout.hpp:32
Definition GraphicsSplitLayout.hpp:9
Summary of a page, selects it on click.
Definition GraphicsStripDetailLayout.hpp:14
Definition GraphicsStripDetailLayout.hpp:55
Definition GraphicsTabLayout.hpp:12
Definition GraphicsGridLayout.hpp:43
void setRowsSelectable(bool selectable)
Rows must be GraphicsSelectableRow.
Definition GraphicsGridLayout.cpp:162
Definition GraphicsBoxLayout.hpp:22
FactoryType * get(const key_type &k) const noexcept
Get a particular factory from its ConcreteKey.
Definition InterfaceList.hpp:128
Definition RectItem.hpp:12
Definition TextItem.hpp:28
Definition score-plugin-avnd/Crousti/Layer.hpp:41
Definition Controls.hpp:27
double normalizedValue(const Process::ControlInlet &port, const ossia::value &v)
A control's value, normalized over its range.
Definition ControlViews.hpp:67
Base toolkit upon which the software is built.
Definition Application.cpp:83
QString locateFilePath(const QString &filename) noexcept
Definition File.cpp:84
QString relativizeFilePath(const QString &filename, const score::DocumentContext &ctx) noexcept
Definition File.cpp:122
STL namespace.
Static metadata implementation.
Definition lib/score/tools/Metadata.hpp:36
Definition WidgetInlets.hpp:517
Definition ProcessContext.hpp:12
Definition PortFactory.hpp:49
Overrides of a control's default presentation in a layout.
Definition PortFactory.hpp:25
std::optional< QString > label
Display only: the port name is unchanged.
Definition PortFactory.hpp:35
Definition WidgetInlets.hpp:568
Definition WidgetInlets.hpp:402
Definition plugins/score-lib-process/Control/Layout.hpp:16
Definition ObjectMatches.hpp:6
Definition score-plugin-avnd/Crousti/Layer.hpp:230
Definition score-plugin-avnd/Crousti/Layer.hpp:297
static QString displayText(const Process::ControlInlet &port, const ossia::value &v, const QFontMetricsF &metrics, qreal max_width=72.)
What a display shows for a value: an entry's name, or a number.
Definition score-plugin-avnd/Crousti/Layer.hpp:572
int tableDepth
Depth in titled tables: controls there hide their labels.
Definition score-plugin-avnd/Crousti/Layer.hpp:308
std::map< std::string, std::shared_ptr< score::LayoutSelection >, std::less<> > selections
Named selections, and the one each selecting table drives.
Definition score-plugin-avnd/Crousti/Layer.hpp:311
QObject * connections
Context object of the connections to this build's widgets.
Definition score-plugin-avnd/Crousti/Layer.hpp:305
void setupControl(QGraphicsItem *parent, Port_T *port, const Process::ControlLayout &lay, Item &item)
Definition score-plugin-avnd/Crousti/Layer.hpp:335
void acceptFileDrops(Process::FileChooserBase &port)
Files dropped on the enclosing strip cell set the port.
Definition score-plugin-avnd/Crousti/Layer.hpp:536
score::GraphicsStripCell * enclosingCell() const
The strip cell being built, if any.
Definition score-plugin-avnd/Crousti/Layer.hpp:527
void setupCondition(score::GraphicsLayout &lay, auto... recursive_members)
Definition score-plugin-avnd/Crousti/Layer.hpp:399
Definition MessageBus.hpp:174
Definition MessageBus.hpp:44
Definition score-plugin-avnd/Crousti/Layer.hpp:48
Definition score-plugin-avnd/Crousti/Layer.hpp:61
QObject * uiConnections
Definition score-plugin-avnd/Crousti/Layer.hpp:66
Definition score-plugin-avnd/Crousti/Layer.hpp:192
Definition score-plugin-avnd/Crousti/Layer.hpp:180
Definition DocumentContext.hpp:18