Loading...
Searching...
No Matches
ValueDisplay.hpp
1#pragma once
2#include <State/ValuePrettyPrint.hpp>
3
4#include <Process/Dataflow/Port.hpp>
5#include <Process/Dataflow/PortFactory.hpp>
6#include <Process/Dataflow/PortItem.hpp>
7#include <Process/Process.hpp>
8
9#include <Effect/EffectLayer.hpp>
10#include <Effect/EffectLayout.hpp>
11
12#include <score/application/ApplicationContext.hpp>
13#include <score/model/Skin.hpp>
14
15#include <ossia/dataflow/port.hpp>
16#include <ossia/network/value/format_value.hpp>
17#include <ossia/network/value/value_conversion.hpp>
18
19#include <boost/container/devector.hpp>
20
21#include <QApplication>
22#include <QClipboard>
23#include <QMenu>
24#include <QPainter>
25
26#include <halp/audio.hpp>
27#include <halp/controls.hpp>
28#include <halp/meta.hpp>
29#include <halp/polyfill.hpp>
30#include <halp/static_string.hpp>
31
32#include <cmath>
33
34#include <algorithm>
35#include <iterator>
36#include <optional>
37#include <vector>
38
39namespace Ui::ValueDisplay
40{
41template <halp::static_string lit, typename T>
43{
44 halp_meta(is_event, true)
45 static clang_buggy_consteval auto name() { return std::string_view{lit.value}; }
46
47 T* value{};
48};
49
50struct Node
51{
52 halp_meta(name, "Value display")
53 halp_meta(c_name, "Display")
54 halp_meta(category, "Monitoring")
55 halp_meta(author, "ossia score")
56 halp_meta(manual_url, "")
57 halp_meta(description, "Visualize an input value")
58 halp_meta(uuid, "3f4a41f2-fa39-420f-ab0f-0af6b8409edb")
59 halp_flag(fully_custom_item);
60
61 static constexpr int max_log = 100;
62
63 enum class Format
64 {
65 Ordinary,
66 Pretty,
67 Hex
68 };
69
70 static std::optional<unsigned char> byteValue(float value) noexcept
71 {
72 if(value >= 0.f && value <= 255.f && std::trunc(value) == value)
73 return static_cast<unsigned char>(value);
74 return std::nullopt;
75 }
76
77 static std::optional<unsigned char> byteValue(const ossia::value& value) noexcept
78 {
79 if(auto* number = value.target<int>())
80 {
81 if(*number >= 0 && *number <= 255)
82 return static_cast<unsigned char>(*number);
83 }
84 else if(auto* number = value.target<float>())
85 return byteValue(*number);
86 else if(auto* boolean = value.target<bool>())
87 return static_cast<unsigned char>(*boolean);
88 return std::nullopt;
89 }
90
91 static void printHexValue(std::string& out, const ossia::value& value)
92 {
93 // Match the state value editor's HexEdit: lowercase pairs, spaces, sixteen
94 // bytes per row. Write directly to the retained buffer, not a temporary
95 // QByteArray / QString pair.
96 constexpr char digits[] = "0123456789abcdef";
97 const auto start = out.size();
98 std::size_t count = 0;
99 auto append = [&](unsigned char byte) {
100 if(count != 0)
101 out.push_back(count % 16 == 0 ? '\n' : ' ');
102 out.push_back(digits[byte >> 4]);
103 out.push_back(digits[byte & 0x0f]);
104 ++count;
105 };
106 auto appendNumbers = [&](const auto& numbers) {
107 for(const auto& number : numbers)
108 {
109 auto byte = byteValue(number);
110 if(!byte)
111 return false;
112 append(*byte);
113 }
114 return true;
115 };
116
117 bool valid = true;
118 if(auto* bytes = value.target<std::string>())
119 {
120 for(unsigned char byte : *bytes)
121 append(byte);
122 }
123 else if(auto* numbers = value.target<std::vector<ossia::value>>())
124 valid = appendNumbers(*numbers);
125 else if(auto* numbers = value.target<ossia::vec2f>())
126 valid = appendNumbers(*numbers);
127 else if(auto* numbers = value.target<ossia::vec3f>())
128 valid = appendNumbers(*numbers);
129 else if(auto* numbers = value.target<ossia::vec4f>())
130 valid = appendNumbers(*numbers);
131 else if(auto byte = byteValue(value))
132 append(*byte);
133 else
134 valid = false;
135
136 if(!valid)
137 {
138 // Do not truncate, wrap, flatten or reinterpret non-byte values. A bad
139 // element rejects the entire list, including any already appended bytes.
140 out.resize(start);
141 out += "[not byte data] ";
142 State::printValue(out, value);
143 }
144 else if(count == 0)
145 out += "[empty]";
146 }
147
148 struct
149 {
150 // Reading the port directly rather than through a control input: a control
151 // input is fed with get_data().back(), so only the last value of a tick
152 // would ever be seen, and a burst - a note-off immediately followed by a
153 // note-on, several values written at the same sample - would show up as a
154 // single entry.
155 struct : raw_port<"Input", ossia::value_port>
156 {
157 halp_meta(description, "Values to display, including every event in a tick.")
158 } port;
159
160 struct : halp::spinbox_i32<"Log", halp::range{1, max_log, 1}>
161 {
162 halp_meta(description, "Number of recent values to retain, newest first.")
163 } log;
164
165 struct : halp::combobox_t<"Format", Format>
166 {
167 halp_meta(
168 description,
169 "Ordinary text, indented Pretty text, or Hex bytes. Hex shows raw "
170 "string bytes, including UTF-8 bytes, NUL and high-bit bytes. Numbers "
171 "must be exact integers from 0 to 255; booleans are 00 or 01. Flat "
172 "numeric lists and vectors use one byte per element. Empty strings "
173 "and lists show [empty]. Other values show [not byte data] followed "
174 "by ordinary text; no wrapping, truncation or memory reinterpretation.")
175 struct range
176 {
177 std::string_view values[3]{"Ordinary", "Pretty", "Hex"};
178 Format init{Format::Ordinary};
179 };
180 } format;
181 } inputs;
182
183 struct
184 {
185 // [sequence number of the first entry, values...]
186 struct : halp::val_port<"values", std::optional<ossia::value>>
187 {
188 halp_meta(description, "Recent values and their sequence number for the display.")
189 enum widget
190 {
191 control
192 };
193 } values;
194 } outputs;
195
196 // The engine -> UI queue for control outputs is drained keeping only the last
197 // entry, so a push has to carry everything the layer may still need. That is
198 // never more than the log length, which bounds the ring exactly.
199 boost::container::devector<ossia::value> m_ring;
200 int m_next_seq = 0;
201
202 void prepare(halp::setup) noexcept
203 {
204 m_ring.clear();
205 m_next_seq = 0;
206 }
207
208 void operator()()
209 {
210 outputs.values.value.reset();
211
212 if(!inputs.port.value)
213 return;
214
215 const auto& data = inputs.port.value->get_data();
216 if(data.empty())
217 return;
218
219 // Never more than what the layer displays: with a log of 1 this sends the
220 // last value and nothing else.
221 const int keep = std::clamp(inputs.log.value, 1, max_log);
222
223 for(const ossia::timed_value& tv : data)
224 {
225 m_ring.push_back(tv.value);
226 m_next_seq++;
227 if(std::ssize(m_ring) > keep)
228 m_ring.pop_front();
229 }
230 while(std::ssize(m_ring) > keep)
231 m_ring.pop_front();
232
233 std::vector<ossia::value> payload;
234 payload.reserve(1 + m_ring.size());
235 payload.push_back(int(m_next_seq - std::ssize(m_ring)));
236 for(const auto& v : m_ring)
237 payload.push_back(v);
238
239 outputs.values.value = ossia::value{std::move(payload)};
240 }
241
243 {
244 public:
245 boost::container::devector<ossia::value> values;
246 int m_next_seq = 0;
247
248 Process::ControlInlet* log_inlet{};
249 Process::ControlInlet* format_inlet{};
250
251 // The rendered text. Rebuilt when values, log length or format change,
252 // never on paint; m_buf retains its capacity across rebuilds.
253 QString txt_cache;
254 std::string m_buf;
255
256 int logging() const noexcept
257 {
258 return std::clamp(ossia::convert<int>(log_inlet->value()), 1, max_log);
259 }
260
261 Format format() const noexcept
262 {
263 return format_inlet
264 ? static_cast<Format>(ossia::convert<int>(format_inlet->value()))
265 : Format::Ordinary;
266 }
267
268 void rebuildText()
269 {
270 m_buf.clear();
271 const auto mode = format();
272 for(const auto& line : this->values)
273 {
274 switch(mode)
275 {
276 case Format::Hex:
277 printHexValue(m_buf, line);
278 break;
279 case Format::Pretty:
280 State::prettyPrintValue(m_buf, line);
281 break;
282 default:
283 State::printValue(m_buf, line);
284 break;
285 }
286 m_buf.push_back('\n');
287 }
288 txt_cache = QString::fromUtf8(m_buf.data(), m_buf.size());
289 update();
290 }
291
292 Layer(
293 const Process::ProcessModel& process, const Process::Context& doc,
294 QGraphicsItem* parent)
296 {
297 setAcceptedMouseButtons(Qt::NoButton);
298
299 const Process::PortFactoryList& portFactory
301
302 auto& value_inlet = *process.inlets()[0];
303 if(auto* fact = portFactory.get(value_inlet.concreteKey()))
304 if(auto* port = fact->makePortItem(value_inlet, doc, this, this))
305 port->setPos(0, 5);
306
307 log_inlet = static_cast<Process::ControlInlet*>(process.inlets()[1]);
308 if(process.inlets().size() > 2)
309 format_inlet = qobject_cast<Process::ControlInlet*>(process.inlets()[2]);
310
311 auto* out = static_cast<Process::ControlOutlet*>(process.outlets()[0]);
312 connect(
313 out, &Process::ControlOutlet::valueChanged, this,
314 [this](const ossia::value& v) { on_values(v); });
315
316 connect(
317 log_inlet, &Process::ControlInlet::valueChanged, this,
318 [this](const ossia::value& v) {
319 while(std::ssize(values) > logging())
320 values.pop_back();
321 rebuildText();
322 });
323
324 if(format_inlet)
325 connect(
326 format_inlet, &Process::ControlInlet::valueChanged, this,
327 [this](const ossia::value&) { rebuildText(); });
328 }
329
330 void on_values(const ossia::value& v)
331 {
332 auto* list = v.target<std::vector<ossia::value>>();
333 if(!list || list->size() < 2)
334 return;
335
336 const int base = ossia::convert<int>((*list)[0]);
337 const int n = std::ssize(*list) - 1;
338
339 // The batch is entirely older than what we hold: the process restarted.
340 if(base + n < m_next_seq)
341 {
342 values.clear();
343 m_next_seq = base;
344 }
345
346 for(int i = 0; i < n; i++)
347 {
348 const int seq = base + i;
349 if(seq < m_next_seq)
350 continue;
351 values.push_front((*list)[i + 1]);
352 m_next_seq = seq + 1;
353 }
354
355 while(std::ssize(values) > logging())
356 values.pop_back();
357
358 rebuildText();
359 }
360
361 void reset()
362 {
363 values.clear();
364 m_next_seq = 0;
365 rebuildText();
366 }
367
368 void paint_impl(QPainter* p) const override
369 {
370 if(txt_cache.isEmpty())
371 return;
372
373 const auto& skin = score::Skin::instance();
374 p->setFont(skin.MonoFontSmall);
375 p->setRenderHint(QPainter::Antialiasing, true);
376 p->setPen(skin.Light.main.pen1_solid_flat_miter);
377 p->drawText(boundingRect().adjusted(10, 0, 0, 0), txt_cache);
378 p->setRenderHint(QPainter::Antialiasing, false);
379 }
380 };
381
383 {
384 using Process::EffectLayerPresenter::EffectLayerPresenter;
385 void fillContextMenu(
386 QMenu& menu, QPoint pos, QPointF scenepos,
387 const Process::LayerContextMenuManager&) override
388 {
389 auto cp = menu.addAction(tr("Copy value"));
390 connect(cp, &QAction::triggered, this, [this] {
391 auto& v = *static_cast<Layer*>(this->m_view);
392 qApp->clipboard()->setText(v.txt_cache);
393 });
394 }
395 };
396};
397}
Definition Port.hpp:218
Definition Port.hpp:452
Definition EffectLayer.hpp:33
Definition EffectLayer.hpp:17
Definition LayerContextMenu.hpp:38
Definition PortFactory.hpp:82
The Process class.
Definition score-lib-process/Process/Process.hpp:75
FactoryType * get(const key_type &k) const noexcept
Get a particular factory from its ConcreteKey.
Definition InterfaceList.hpp:128
void printValue(std::string &out, const ossia::value &v)
Single-line rendering of v, identical to fmt::format("{}", v), appended to out.
Definition ValuePrettyPrint.cpp:153
void prettyPrintValue(std::string &out, const ossia::value &v, int depth, const PrettyPrintOptions &opts)
Appends a multi-line, indented rendering of v to out.
Definition ValuePrettyPrint.cpp:147
Definition ProcessContext.hpp:12
Definition ValueDisplay.hpp:243
Definition ValueDisplay.hpp:383
Definition ValueDisplay.hpp:51
Definition ValueDisplay.hpp:43
Definition MIDISync.hpp:127
const T & interfaces() const
Access to a specific interface list.
Definition ApplicationContext.hpp:87