wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ElementEditor.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <cmath>
7
8#include <QDebug>
9#include <QDir>
10#include <QFile>
11#include <QIcon>
12#include <QImageReader>
13#include <QMessageBox>
14#include <QSize>
15#include <QToolButton>
16
17#include "App/Core/Common.h"
25#include "App/Element/IC.h"
28#include "App/Scene/Commands.h"
30#include "App/Scene/Scene.h"
38
40 : QWidget(parent)
41 , m_ui(std::make_unique<ElementEditorUi>())
42{
43 m_ui->setupUi(this);
44 // Start hidden/disabled; setCurrentElements() enables and shows it once a
45 // selection is made in the scene.
46 setEnabled(false);
47 hide();
48
49 // Trigger key accepts a single alphanumeric character (keyboard hotkey for
50 // InputButton elements). The validator prevents multi-char entries.
51 m_ui->lineEditTrigger->setValidator(new QRegularExpressionValidator(QRegularExpression("[a-z]| |[A-Z]|[0-9]"), this));
53
54 // Install the tab navigator on every interactive widget so Tab/Shift+Tab
55 // cycle through scene elements rather than moving focus between UI fields.
56 m_tabNavigator = new ElementTabNavigator(this, this);
57 m_ui->checkBoxLocked->installEventFilter(m_tabNavigator);
58 m_ui->comboBoxAudio->installEventFilter(m_tabNavigator);
59 m_ui->comboBoxColor->installEventFilter(m_tabNavigator);
60 m_ui->comboBoxInputSize->installEventFilter(m_tabNavigator);
61 m_ui->comboBoxOutputSize->installEventFilter(m_tabNavigator);
62 m_ui->comboBoxValue->installEventFilter(m_tabNavigator);
63 m_ui->sliderDelay->installEventFilter(m_tabNavigator);
64 m_ui->doubleSpinBoxFrequency->installEventFilter(m_tabNavigator);
65 m_ui->sliderVolume->installEventFilter(m_tabNavigator);
66 m_ui->comboBoxWirelessMode->installEventFilter(m_tabNavigator);
67 m_ui->lineEditElementLabel->installEventFilter(m_tabNavigator);
68 m_ui->lineEditTrigger->installEventFilter(m_tabNavigator);
69 // The truth table editor is a floating dialog rather than embedded in the
70 // panel so it doesn't push other properties off-screen for large tables.
71 m_tableBox = new QDialog(this);
72 m_tableBox->setWindowFlags(Qt::Window);
73 m_tableBox->setWindowTitle(tr("Truth Table"));
74 m_tableBox->setModal(true);
75 auto *tableLayout = new QGridLayout(m_tableBox);
76 m_table = new QTableWidget(this);
77 tableLayout->addWidget(m_table);
78 m_tableBox->setLayout(tableLayout);
79
80 connect(m_table, &QTableWidget::cellDoubleClicked, this, &ElementEditor::setTruthTableProposition);
81 connect(m_ui->checkBoxLocked, &QCheckBox::clicked, this, &ElementEditor::inputLocked);
82 connect(m_ui->comboBoxAudio, qOverload<int>(&QComboBox::currentIndexChanged), this, &ElementEditor::apply);
83 connect(m_ui->comboBoxColor, qOverload<int>(&QComboBox::currentIndexChanged), this, &ElementEditor::apply);
84 connect(m_ui->comboBoxInputSize, qOverload<int>(&QComboBox::currentIndexChanged), this, &ElementEditor::inputIndexChanged);
85 connect(m_ui->comboBoxOutputSize, qOverload<int>(&QComboBox::currentIndexChanged), this, &ElementEditor::outputIndexChanged);
86 connect(m_ui->comboBoxValue, &QComboBox::currentTextChanged, this, &ElementEditor::outputValueChanged);
87 connect(m_ui->sliderDelay, qOverload<int>(&QSlider::valueChanged), this, &ElementEditor::apply);
88 connect(m_ui->doubleSpinBoxFrequency, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &ElementEditor::apply);
89 connect(m_ui->sliderVolume, qOverload<int>(&QSlider::valueChanged), this, &ElementEditor::apply);
90 connect(m_ui->comboBoxWirelessMode, qOverload<int>(&QComboBox::currentIndexChanged), this, &ElementEditor::apply);
91 connect(m_ui->lineEditElementLabel, &QLineEdit::textChanged, this, &ElementEditor::apply);
92 connect(m_ui->lineEditTrigger, &QLineEdit::textChanged, this, &ElementEditor::triggerChanged);
93 connect(m_ui->lineEditBlobName, &QLineEdit::editingFinished, this, &ElementEditor::blobNameEditingFinished);
94 connect(m_ui->pushButtonAudioBox, &QPushButton::clicked, this, &ElementEditor::audioBox);
95 connect(m_ui->pushButtonChangeAppearance, &QPushButton::clicked, this, &ElementEditor::updateElementAppearance);
96 connect(m_ui->pushButtonDefaultAppearance, &QPushButton::clicked, this, &ElementEditor::defaultAppearance);
97 connect(m_ui->pushButtonTruthTable, &QPushButton::clicked, this, &ElementEditor::truthTable);
98}
99
103
104// Helper: set visibility/enabled state of a label+widget pair together.
105static void setSection(bool show, QWidget *label, QWidget *widget)
106{
107 label->setVisible(show);
108 widget->setVisible(show);
109 widget->setEnabled(show);
110}
111
112// Helper: manage the "many values" placeholder in a QComboBox.
113// Always ensures the placeholder item exists. If hasFeature && hasSame, removes
114// the placeholder and returns true (caller should then set the real value).
115// If hasFeature && !hasSame, selects the placeholder text and returns false.
116static bool prepareCombo(QComboBox *cb, bool hasFeature, bool hasSame, const QString &many)
117{
118 if (cb->findText(many) == -1) {
119 cb->addItem(many);
120 }
121
122 if (!hasFeature) {
123 return false;
124 }
125
126 if (hasSame) {
127 cb->removeItem(cb->findText(many));
128 return true;
129 }
130
131 cb->setCurrentText(many);
132 return false;
133}
134
135void ElementEditor::contextMenu(QPoint screenPos, QGraphicsItem *itemAtMouse)
136{
138 screenPos, itemAtMouse, m_caps, m_elements, m_ui->comboBoxColor, m_scene,
139 [this](QUndoCommand *cmd) { emit sendCommand(cmd); },
140 [this] { renameAction(); },
141 [this] { changeTriggerAction(); },
142 [this] { updateElementAppearance(); },
143 [this] { m_isDefaultAppearance = true; m_isUpdatingAppearance = true; apply(); },
144 [this] { m_ui->doubleSpinBoxFrequency->setFocus(); },
145 // IC sub-circuit actions
146 [this] {
147 if (m_elements.isEmpty()) return;
148 auto *elm = m_elements.first();
149 if (elm->isEmbedded()) {
150 emit editSubcircuitRequested(elm->blobName(), elm->id());
151 } else if (elm->elementType() == ElementType::IC) {
152 auto *ic = static_cast<IC *>(elm);
153 emit openSubcircuitFileRequested(ic->file());
154 }
155 },
156 [this] { emit embedSubcircuitRequested(); },
157 [this] { emit extractToFileRequested(); }
158 );
159}
160
162{
163 m_ui->lineEditElementLabel->setFocus();
164 m_ui->lineEditElementLabel->selectAll();
165}
166
168{
169 m_ui->lineEditTrigger->setFocus();
170 m_ui->lineEditTrigger->selectAll();
171}
172
173void ElementEditor::rebuildAppearanceStateTiles(const QList<std::pair<int, QString>> &states)
174{
175 // Drop the previous element's tiles; QButtonGroup auto-detaches destroyed buttons.
176 while (auto *item = m_ui->gridLayoutAppearanceStates->takeAt(0)) {
177 delete item->widget();
178 delete item;
179 }
180
181 constexpr int columns = 4;
182 constexpr QSize tileSize(32, 32);
183 constexpr QSize iconSize(24, 24);
184
185 for (int i = 0; i < states.size(); ++i) {
186 const auto &[index, label] = states[i];
187 auto *tile = new QToolButton(m_ui->widgetAppearanceStates);
188 tile->setCheckable(true);
189 tile->setAutoRaise(true);
190 tile->setFixedSize(tileSize);
191 tile->setIconSize(iconSize);
192 tile->setIcon(QIcon(m_elements[0]->appearancePreviewPixmap(index, iconSize)));
193 tile->setToolTip(label);
194 tile->setProperty("appearanceStateIndex", index);
195 m_ui->buttonGroupAppearanceStates->addButton(tile);
196 m_ui->gridLayoutAppearanceStates->addWidget(tile, i / columns, i % columns);
197 }
198
199 // Default-select the first tile, matching the combobox's old implicit currentIndex 0.
200 if (auto *item = m_ui->gridLayoutAppearanceStates->itemAtPosition(0, 0)) {
201 if (auto *firstTile = qobject_cast<QToolButton *>(item->widget())) {
202 firstTile->setChecked(true);
203 }
204 }
205}
206
208{
209 sentryBreadcrumb("ui", QStringLiteral("Element appearance dialog"));
210 const QString imageFilter = tr("Images") + " (*." + QImageReader::supportedImageFormats().join(" *.") + ")";
211 const QString fileName = FileDialogs::provider()->getOpenFileName(this, tr("Open File"), QString(), imageFilter);
212
213 if (fileName.isEmpty()) {
214 return;
215 }
216
217 qCDebug(zero) << "File name: " << fileName;
218
219 // If a specific appearance state is selected in the tile grid, use setAppearanceAt()
220 // to target that index directly instead of relying on the element's current state.
221 if (m_ui->widgetAppearanceStates->isVisible() && m_ui->buttonGroupAppearanceStates->checkedButton()) {
222 const int appearanceIndex = m_ui->buttonGroupAppearanceStates->checkedButton()->property("appearanceStateIndex").toInt();
223
224 // Snapshot and apply via undo command
225 const bool needsMacro = m_scene && m_elements.size() > 1;
226 if (needsMacro) {
227 m_scene->undoStack()->beginMacro(tr("Change appearance"));
228 }
229 for (auto *elm : std::as_const(m_elements)) {
230 QByteArray oldData;
231 {
232 QDataStream stream(&oldData, QIODevice::WriteOnly);
234 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
235 }
236 elm->setAppearanceAt(appearanceIndex, fileName);
237 if (m_scene) {
238 m_scene->receiveCommand(new UpdateCommand({elm}, oldData, m_scene));
239 }
240 }
241 if (needsMacro) {
242 m_scene->undoStack()->endMacro();
243 }
244
245 // The tile icons were built from the pre-change appearance; refresh the edited tile in
246 // place so it shows the new image immediately. Without this it keeps the old preview
247 // until the element is reselected (which rebuilds the whole grid).
248 if (auto *checked = qobject_cast<QToolButton *>(m_ui->buttonGroupAppearanceStates->checkedButton())) {
249 checked->setIcon(QIcon(m_elements[0]->appearancePreviewPixmap(appearanceIndex, checked->iconSize())));
250 }
251 return;
252 }
253
254 m_isUpdatingAppearance = true;
255 m_appearanceName = fileName;
256 m_isDefaultAppearance = false;
257 apply();
258}
259
261{
262 // Disconnect from the old scene first; passing nullptr is valid and is used
263 // when the active tab changes (disconnectTab in MainWindow).
264 if (m_scene) {
265 disconnect(m_scene, &QGraphicsScene::selectionChanged, this, &ElementEditor::selectionChanged);
266 disconnect(m_scene, &Scene::contextMenuPos, this, &ElementEditor::contextMenu);
268 disconnect(this, &ElementEditor::sendCommand, m_scene, &Scene::receiveCommand);
269 }
270
271 m_scene = scene;
272
273 if (m_scene) {
274 connect(m_scene, &QGraphicsScene::selectionChanged, this, &ElementEditor::selectionChanged);
275 connect(m_scene, &Scene::contextMenuPos, this, &ElementEditor::contextMenu);
276 connect(m_scene, &Scene::truthTableElementChanged, this, [this](GraphicElement *) { truthTable(); });
277 connect(this, &ElementEditor::sendCommand, m_scene, &Scene::receiveCommand);
278 }
279}
280
282{
283 // Block signals while rebuilding to avoid triggering apply() for each
284 // addItem() call, which would create spurious undo commands.
285 QSignalBlocker blocker(m_ui->comboBoxColor);
286 m_ui->comboBoxColor->clear();
287 // item data stores the untranslated English colour name used internally
288 // by GraphicElement::setColor(); item text is the translated display name.
289 m_ui->comboBoxColor->addItem(QIcon(QPixmap(":/Components/Output/Led/WhiteLed.svg")), tr("White"), "White");
290 m_ui->comboBoxColor->addItem(QIcon(QPixmap(":/Components/Output/Led/RedLed.svg")), tr("Red"), "Red");
291 m_ui->comboBoxColor->addItem(QIcon(QPixmap(":/Components/Output/Led/GreenLed.svg")), tr("Green"), "Green");
292 m_ui->comboBoxColor->addItem(QIcon(QPixmap(":/Components/Output/Led/BlueLed.svg")), tr("Blue"), "Blue");
293 m_ui->comboBoxColor->addItem(QIcon(QPixmap(":/Components/Output/Led/PurpleLed.svg")), tr("Purple"), "Purple");
294}
295
297{
298 m_ui->retranslateUi(this);
299 // Color names are translated strings; rebuild the combo box so current-language
300 // names are shown.
302
303 // Refresh the editor with the current selection in case any visible text
304 // (e.g. "Many values") needs to be re-translated.
305 if (m_scene) {
306 selectionChanged();
307 }
308}
309
310void ElementEditor::setCurrentElements(const QList<GraphicElement *> &elements)
311{
312 m_elements = elements;
313
314 if (elements.isEmpty()) {
315 m_caps = {};
316 hide();
317 m_ui->lineEditElementLabel->setText("");
318 return;
319 }
320
321 show();
322 // Disable during update to prevent partial changes firing as undo commands.
323 setEnabled(false);
324 m_caps = ::computeCapabilities(m_elements);
325 applyCapabilitiesToUi();
326 setEnabled(true);
327}
328
329void ElementEditor::applyCapabilitiesToUi()
330{
331 const auto &c = m_caps;
332 auto *firstElement = m_elements.constFirst();
333 auto *firstInput = qobject_cast<GraphicElementInput *>(firstElement);
334
335 /* Element type title */
336 m_ui->groupBox->setTitle(ElementFactory::typeToTitleText(c.elementType));
337
338 /* Label */
339 setSection(c.hasLabel, m_ui->labelLabels, m_ui->lineEditElementLabel);
340 if (c.hasLabel) {
341 m_ui->lineEditElementLabel->setText(c.hasSameLabel ? firstElement->label() : m_manyLabels);
342 }
343
344 /* Color */
345 setSection(c.hasColors, m_ui->labelColor, m_ui->comboBoxColor);
346 if (prepareCombo(m_ui->comboBoxColor, c.hasColors, c.hasSameColors, m_manyColors)) {
347 m_ui->comboBoxColor->setCurrentIndex(m_ui->comboBoxColor->findData(firstElement->color()));
348 }
349
350 /* Sound */
351 setSection(c.hasAudio, m_ui->labelAudio, m_ui->comboBoxAudio);
352 if (prepareCombo(m_ui->comboBoxAudio, c.hasAudio, c.hasSameAudio, m_manyAudios)) {
353 m_ui->comboBoxAudio->setCurrentText(firstElement->audio());
354 }
355
356 /* Volume */
357 setSection(c.hasVolume, m_ui->labelVolume, m_ui->sliderVolume);
358 if (c.hasVolume && c.hasSameVolume) {
359 m_ui->sliderVolume->setValue(static_cast<int>(firstElement->volume() * 100.0f));
360 }
361
362 /* Frequency */
363 setSection(c.hasFrequency, m_ui->labelFrequency, m_ui->doubleSpinBoxFrequency);
364 if (c.hasFrequency) {
365 // Buzzer uses audible tone range; Clock uses low oscillation range.
366 const bool isBuzzer = (c.hasSameType && c.elementType == ElementType::Buzzer);
367 const double minFreq = isBuzzer ? 20.0 : 0.1;
368 const double maxFreq = isBuzzer ? 20000.0 : 50.0;
369 const double step = isBuzzer ? 10.0 : 0.1;
370 const int decimals = isBuzzer ? 0 : 1;
371 const QString suffix = isBuzzer ? tr(" Hz") : tr(" Hz");
372
373 m_ui->doubleSpinBoxFrequency->setDecimals(decimals);
374 m_ui->doubleSpinBoxFrequency->setSingleStep(step);
375 m_ui->doubleSpinBoxFrequency->setMaximum(maxFreq);
376 m_ui->doubleSpinBoxFrequency->setSuffix(suffix);
377
378 if (c.hasSameFrequency) {
379 m_ui->doubleSpinBoxFrequency->setMinimum(minFreq);
380 m_ui->doubleSpinBoxFrequency->setSpecialValueText({});
381 m_ui->doubleSpinBoxFrequency->setValue(firstElement->frequency());
382 } else {
383 m_ui->doubleSpinBoxFrequency->setMinimum(0.0);
384 m_ui->doubleSpinBoxFrequency->setSpecialValueText(m_manyFreq);
385 m_ui->doubleSpinBoxFrequency->setValue(0.0);
386 }
387 }
388
389 /* Delay */
390 setSection(c.hasDelay, m_ui->labelDelay, m_ui->sliderDelay);
391 if (c.hasDelay) {
392 // Convert delay value (fraction of period, -0.5 to 0.5) to slider value (-4 to 4).
393 m_ui->sliderDelay->setValue(c.hasSameDelay ? static_cast<int>(firstElement->delay() * 8.0) : 0);
394 }
395
396 /* Input size */
397 {
398 // Block signals while rebuilding so the synthetic addItem/setCurrentIndex
399 // calls don't fire inputIndexChanged — that slot would push spurious
400 // ChangePortSizeCommands and (more importantly here) flood the Sentry
401 // breadcrumb buffer with "Input count: -1 / 0" entries that crowd out
402 // the user's real actions.
403 QSignalBlocker blocker(m_ui->comboBoxInputSize);
404 m_ui->comboBoxInputSize->clear();
405 setSection(c.canChangeInputSize, m_ui->labelInputs, m_ui->comboBoxInputSize);
406 if (c.canChangeInputSize && c.hasSameType && c.elementType == ElementType::Mux) {
407 // For Mux, the user picks the number of data inputs (2–8); select lines
408 // are computed as ceil(log2(dataInputs)) and added internally. The item
409 // display text shows data inputs while the item data carries the total.
410 for (int dataInputs = 2; dataInputs <= 8; ++dataInputs) {
411 int selectLines = 1;
412 while ((1 << selectLines) < dataInputs) { ++selectLines; }
413 const int totalInputs = dataInputs + selectLines;
414 if (totalInputs >= c.minimumInputs && totalInputs <= c.maximumInputs) {
415 m_ui->comboBoxInputSize->addItem(QString::number(dataInputs), totalInputs);
416 }
417 }
418 } else {
419 for (int port = c.minimumInputs; port <= c.maximumInputs; ++port) {
420 m_ui->comboBoxInputSize->addItem(QString::number(port), port);
421 }
422 }
423 if (prepareCombo(m_ui->comboBoxInputSize, c.canChangeInputSize, c.hasSameInputSize, m_manyIS)) {
424 const int idx = m_ui->comboBoxInputSize->findData(firstElement->inputSize());
425 if (idx >= 0) { m_ui->comboBoxInputSize->setCurrentIndex(idx); }
426 }
427 }
428
429 /* Output size */
430 {
431 QSignalBlocker blocker(m_ui->comboBoxOutputSize);
432 m_ui->comboBoxOutputSize->clear();
433 setSection(c.canChangeOutputSize, m_ui->labelOutputs, m_ui->comboBoxOutputSize);
434 if (c.canChangeOutputSize) {
435 if (c.hasSameType && c.elementType == ElementType::Demux) {
436 // Demux: show 2-8 output options, mirroring Mux's 2-8 data inputs.
437 for (int n = c.minimumOutputs; n <= c.maximumOutputs; ++n) {
438 m_ui->comboBoxOutputSize->addItem(QString::number(n), n);
439 }
440 } else {
441 // For generic multi-output elements (e.g. Node) offer a curated set
442 // of common bus widths rather than every value in [min, max].
443 for (int n : {2, 3, 4, 6, 8, 10, 12, 16}) {
444 m_ui->comboBoxOutputSize->addItem(QString::number(n), n);
445 }
446 }
447 }
448 if (prepareCombo(m_ui->comboBoxOutputSize, c.canChangeOutputSize && !c.hasTruthTable,
449 c.hasSameOutputSize, m_manyOS)) {
450 const int idx = m_ui->comboBoxOutputSize->findData(firstElement->outputSize());
451 if (idx >= 0) { m_ui->comboBoxOutputSize->setCurrentIndex(idx); }
452 }
453 }
454
455 /* Output value (hidden for momentary inputs like InputButton) */
456 m_ui->comboBoxValue->clear();
457 setSection(c.hasLatchedValue, m_ui->labelValue, m_ui->comboBoxValue);
458 if (c.hasLatchedValue) {
459 // Binary inputs have outputSize == 1 but must offer values 0 and 1,
460 // so bump the upper bound to 2 in that case.
461 int maxOut = c.maxCurrentOutputSize;
462 if (maxOut == 1) { ++maxOut; }
463 for (int val = 0; val < maxOut; ++val) {
464 m_ui->comboBoxValue->addItem(QString::number(val), val);
465 }
466 }
467 if (prepareCombo(m_ui->comboBoxValue, c.hasLatchedValue, c.hasSameOutputValue, m_manyOV)) {
468 m_ui->comboBoxValue->setCurrentText(QString::number(firstInput->outputValue()));
469 }
470
471 /* Input locked */
472 setSection(c.hasOnlyInputs, m_ui->labelLocked, m_ui->checkBoxLocked);
473 if (c.hasOnlyInputs) {
474 // When a multi-selection has mixed lock states, show a partially-checked
475 // state rather than incorrectly representing all as locked or unlocked.
476 if (c.sameCheckState) {
477 m_ui->checkBoxLocked->setCheckState(firstInput->isLocked() ? Qt::Checked : Qt::Unchecked);
478 } else {
479 m_ui->checkBoxLocked->setCheckState(Qt::PartiallyChecked);
480 }
481 }
482
483 /* Trigger */
484 setSection(c.hasTrigger, m_ui->labelTrigger, m_ui->lineEditTrigger);
485 if (c.hasTrigger) {
486 m_ui->lineEditTrigger->setText(c.hasSameTrigger ? firstElement->trigger().toString() : m_manyTriggers);
487 }
488
489 /* AudioBox */
490 m_ui->pushButtonAudioBox->setVisible(c.hasAudioBox);
491 m_ui->pushButtonAudioBox->setEnabled(c.hasAudioBox);
492 m_ui->labelAudioBox->setVisible(c.hasAudioBox);
493 m_ui->lineCurrentAudioBox->setVisible(c.hasAudioBox);
494 if (c.hasAudioBox) {
495 m_ui->lineCurrentAudioBox->setText(m_elements.size() > 1 ? m_manyAudios : m_elements[0]->audio());
496 }
497
498 /* TruthTable */
499 m_ui->pushButtonTruthTable->setVisible(c.hasTruthTable);
500 m_ui->pushButtonTruthTable->setEnabled(c.hasTruthTable);
501
502 /* Appearance */
503 m_ui->pushButtonChangeAppearance->setVisible(c.canChangeAppearance);
504 m_ui->pushButtonDefaultAppearance->setVisible(c.canChangeAppearance);
505
506 // Populate the appearance state icon grid for multi-state elements
507 if (c.canChangeAppearance && m_elements.size() == 1) {
508 const auto states = m_elements[0]->appearanceStates();
509 const bool multiState = states.size() > 1;
510 m_ui->labelAppearanceState->setVisible(multiState);
511 m_ui->widgetAppearanceStates->setVisible(multiState);
512 if (multiState) {
513 rebuildAppearanceStateTiles(states);
514 }
515 } else {
516 m_ui->labelAppearanceState->setVisible(false);
517 m_ui->widgetAppearanceStates->setVisible(false);
518 }
519
520 /* Wireless mode — Node elements only */
521 setSection(c.hasWirelessMode, m_ui->labelWirelessMode, m_ui->comboBoxWirelessMode);
522 if (c.hasWirelessMode && !m_elements.isEmpty()) {
523 auto *first = qobject_cast<Node *>(m_elements.constFirst());
524 const bool sameMode = std::all_of(m_elements.cbegin(), m_elements.cend(), [&](GraphicElement *elm) {
525 const auto *n = qobject_cast<Node *>(elm);
526 return n && n->wirelessMode() == first->wirelessMode();
527 });
528 if (prepareCombo(m_ui->comboBoxWirelessMode, true, sameMode, m_manyWirelessModes) && first) {
529 QSignalBlocker blocker(m_ui->comboBoxWirelessMode);
530 m_ui->comboBoxWirelessMode->setCurrentIndex(static_cast<int>(first->wirelessMode()));
531 }
532 }
533
534 /* Blob name — embedded ICs only */
535 setSection(c.isEmbedded, m_ui->labelBlobName, m_ui->lineEditBlobName);
536 if (c.isEmbedded && !m_elements.isEmpty()) {
537 const auto *firstElm = m_elements.constFirst();
538 const bool sameBlobName = std::all_of(m_elements.cbegin(), m_elements.cend(), [&](GraphicElement *elm) {
539 return elm->blobName() == firstElm->blobName();
540 });
541 QSignalBlocker blocker(m_ui->lineEditBlobName);
542 m_ui->lineEditBlobName->setText(sameBlobName ? firstElement->blobName() : m_manyLabels);
543 }
544
545 /* Section visibility — hide a section's group box entirely when none of its
546 rows apply to the current selection, so the panel doesn't show empty
547 titled boxes. */
548 m_ui->groupBoxIdentity->setVisible(c.hasLabel || c.hasColors);
549 m_ui->groupBoxPorts->setVisible(c.canChangeInputSize || c.canChangeOutputSize || c.hasLatchedValue || c.hasOnlyInputs);
550 m_ui->groupBoxTiming->setVisible(c.hasFrequency || c.hasDelay);
551 m_ui->groupBoxSound->setVisible(c.hasAudio || c.hasAudioBox || c.hasVolume);
552 m_ui->groupBoxInteraction->setVisible(c.hasTrigger || c.hasTruthTable || c.hasWirelessMode);
553 m_ui->groupBoxAppearanceSection->setVisible(c.canChangeAppearance || c.isEmbedded);
554}
555
556void ElementEditor::selectionChanged()
557{
558 setCurrentElements(m_scene->selectedElements());
559}
560
561void ElementEditor::applyProperty(GraphicElement *elm, PropertyDescriptor::Type type)
562{
563 switch (type) {
565 if (m_ui->lineEditElementLabel->text() != m_manyLabels) {
566 elm->setLabel(m_ui->lineEditElementLabel->text());
567 }
568 break;
570 if (m_ui->comboBoxColor->currentData().isValid()) {
571 elm->setColor(m_ui->comboBoxColor->currentData().toString());
572 }
573 break;
575 if (m_ui->doubleSpinBoxFrequency->text() != m_manyFreq) {
576 elm->setFrequency(m_ui->doubleSpinBoxFrequency->value());
577 }
578 break;
580 // Convert slider value (-4 to 4) to phase delay (-0.5 to 0.5 as fraction of period).
581 const double delayFraction = m_ui->sliderDelay->value() / 8.0;
582 elm->setDelay(delayFraction);
583 break;
584 }
586 if (m_ui->comboBoxAudio->currentText() != m_manyAudios) {
587 elm->setAudio(m_ui->comboBoxAudio->currentText());
588 }
589 break;
591 // Force uppercase so the stored key sequence matches Qt's key names and is
592 // compatible across keyboard layouts.
593 if (m_ui->lineEditTrigger->text() != m_manyTriggers && m_ui->lineEditTrigger->text().size() <= 1) {
594 elm->setTrigger(QKeySequence(m_ui->lineEditTrigger->text()));
595 }
596 break;
598 if (m_isUpdatingAppearance) {
599 elm->setAppearance(m_isDefaultAppearance, m_appearanceName);
600 }
601 break;
603 if (m_ui->comboBoxWirelessMode->currentText() != m_manyWirelessModes) {
604 if (auto *node = qobject_cast<Node *>(elm)) {
605 node->setWirelessMode(static_cast<WirelessMode>(m_ui->comboBoxWirelessMode->currentIndex()));
606 }
607 }
608 break;
610 elm->setVolume(static_cast<float>(m_ui->sliderVolume->value()) / 100.0f);
611 break;
614 // Managed by their own dialogs (audioBox() / truthTable()); not applied here.
615 break;
616 }
617}
618
619void ElementEditor::apply()
620{
621 sentryBreadcrumb("ui", QStringLiteral("Element properties applied"));
622 qCDebug(three) << "Apply.";
623
624 // m_scene can be null while m_elements still holds the previous tab's
625 // selection (setScene(nullptr) on tab change) — applying would dereference
626 // the null scene below.
627 if (m_elements.isEmpty() || !isEnabled() || !m_scene) {
628 return;
629 }
630
631 // Reject label changes that would create a duplicate wireless Tx channel.
632 // A Tx node's label is its channel name — two Tx nodes on the same label
633 // means one silently receives no signal, which is always a user error.
634 if (m_scene && m_caps.hasWirelessMode) {
635 const QString newLabel = m_ui->lineEditElementLabel->text();
636 const bool labelChanging = (newLabel != m_manyLabels);
637 const WirelessMode newMode = static_cast<WirelessMode>(m_ui->comboBoxWirelessMode->currentIndex());
638 const bool modeChanging = (m_ui->comboBoxWirelessMode->currentText() != m_manyWirelessModes);
639
640 for (auto *elm : std::as_const(m_elements)) {
641 auto *node = qobject_cast<Node *>(elm);
642 if (!node) continue;
643
644 const QString candidateLabel = labelChanging ? newLabel : node->label();
645 const WirelessMode candidateMode = modeChanging ? newMode : node->wirelessMode();
646
647 if (candidateMode != WirelessMode::Tx || candidateLabel.isEmpty()) continue;
648
649 for (auto *other : m_scene->elements()) {
650 if (other == elm) continue;
651 auto *otherNode = qobject_cast<Node *>(other);
652 if (!otherNode || otherNode->wirelessMode() != WirelessMode::Tx) continue;
653 if (otherNode->label() == candidateLabel) {
654 QMessageBox::warning(this,
655 tr("Duplicate Wireless Channel"),
656 tr("A Tx node with label \"%1\" already exists.\n"
657 "Each wireless channel must have a unique label.").arg(candidateLabel));
658 update();
659 return;
660 }
661 }
662 }
663 }
664
665 // Collect connections that will be severed by wireless mode changes.
666 // setWirelessMode() hides the replaced port but does not delete connections
667 // itself — that is handled here via DeleteItemsCommand so undo can restore them.
668 QList<QGraphicsItem *> wirelessConnsToDelete;
669 if (m_scene && m_caps.hasWirelessMode) {
670 const WirelessMode newMode = static_cast<WirelessMode>(m_ui->comboBoxWirelessMode->currentIndex());
671 const bool modeChanging = (m_ui->comboBoxWirelessMode->currentText() != m_manyWirelessModes);
672
673 if (modeChanging) {
674 for (auto *elm : std::as_const(m_elements)) {
675 auto *node = qobject_cast<Node *>(elm);
676 if (!node) continue;
677
678 Port *port = (newMode == WirelessMode::Rx) ? static_cast<Port *>(node->inputPort())
679 : (newMode == WirelessMode::Tx) ? static_cast<Port *>(node->outputPort())
680 : nullptr;
681 if (port) {
682 for (auto *conn : port->connections()) {
683 wirelessConnsToDelete.append(static_cast<QGraphicsItem *>(conn));
684 }
685 }
686 }
687 }
688 }
689
690 // Snapshot current state into oldData before modifying anything.
691 // UpdateCommand uses oldData for undo and captures the post-modification
692 // state internally as newData when it is constructed.
693 QByteArray oldData;
694 QDataStream stream(&oldData, QIODevice::WriteOnly);
696
697 for (auto *elm : std::as_const(m_elements)) {
698 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
699 for (const auto &prop : elm->editableProperties()) {
700 applyProperty(elm, prop.type);
701 }
702 }
703
704 // Reset the one-shot appearance update flag after applying to all elements.
705 if (m_isUpdatingAppearance) {
706 m_isUpdatingAppearance = false;
707 }
708
709 // When wireless mode changes sever connections, group the property update
710 // and the delete into a single undo macro so both are undone together.
711 const bool needsMacro = !wirelessConnsToDelete.isEmpty();
712 if (needsMacro) {
713 m_scene->undoStack()->beginMacro(tr("Change wireless mode"));
714 }
715
716 m_scene->receiveCommand(new UpdateCommand(m_elements, oldData, m_scene));
717
718 if (needsMacro) {
719 m_scene->receiveCommand(new DeleteItemsCommand(wirelessConnsToDelete, m_scene));
720 m_scene->undoStack()->endMacro();
721 }
722}
723
724void ElementEditor::blobNameEditingFinished()
725{
726 if (m_elements.isEmpty() || !m_caps.isEmbedded) {
727 return;
728 }
729
730 const QString newBlobName = m_ui->lineEditBlobName->text().trimmed();
731 const QString oldBlobName = m_elements.first()->blobName();
732
733 // editingFinished fires on plain focus-out too, not just on an actual edit — guard against
734 // the multi-selection placeholder as well as an empty/unchanged field so clicking into and
735 // out of the field without typing anything never renames.
736 if (newBlobName.isEmpty() || newBlobName == oldBlobName || newBlobName == m_manyLabels) {
737 return;
738 }
739
740 // Reject a rename that collides with a different, already-registered blob: ICRegistry::
741 // renameBlob() would silently overwrite that other blob's bytes with this IC's circuit,
742 // permanently destroying it with no undo path back. Refuse before pushing anything, mirroring
743 // the duplicate-wireless-Tx-channel guard above.
744 if (m_scene->icRegistry()->hasBlob(newBlobName)) {
745 QMessageBox::warning(this,
746 tr("Duplicate IC Name"),
747 tr("An embedded IC named \"%1\" already exists.\n"
748 "Choose a different name.").arg(newBlobName));
749 return;
750 }
751
752 // Renaming is its own independent, immediately-applied action — pushed standalone rather
753 // than folded into apply()'s UpdateCommand, so undo/redo never has to reconcile a generic
754 // property snapshot against the IC registry's shared blob-name state.
755 m_scene->receiveCommand(new RenameBlobCommand(oldBlobName, newBlobName, m_scene));
756}
757
758void ElementEditor::inputIndexChanged(const int index)
759{
760 if (m_elements.isEmpty() || !isEnabled()) {
761 return;
762 }
763 sentryBreadcrumb("ui", QStringLiteral("Input count: %1").arg(index));
764
765 if (m_caps.canChangeInputSize && (m_ui->comboBoxInputSize->currentText() != m_manyIS)) {
766 emit sendCommand(new ChangePortSizeCommand(m_elements, m_ui->comboBoxInputSize->currentData().toInt(), m_scene, true));
767 }
768
769 qCDebug(zero) << "Input size changed to " << index;
770 update();
771}
772
773void ElementEditor::outputIndexChanged(const int index)
774{
775 if (m_elements.isEmpty() || !isEnabled()) {
776 return;
777 }
778 sentryBreadcrumb("ui", QStringLiteral("Output count: %1").arg(index));
779
780 if (m_caps.canChangeOutputSize && (m_ui->comboBoxOutputSize->currentText() != m_manyOS)) {
781 emit sendCommand(new ChangePortSizeCommand(m_elements, m_ui->comboBoxOutputSize->currentData().toInt(), m_scene, false));
782 }
783
784 qCDebug(zero) << "Output size changed to " << index;
785 update();
786}
787
788void ElementEditor::outputValueChanged(const QString &value)
789{
790 if (m_elements.isEmpty() || !isEnabled()) {
791 return;
792 }
793
794 const int newValue = value.toInt();
795
796 for (auto *elm : std::as_const(m_elements)) {
797 if (elm->elementType() == ElementType::InputRotary) {
798 // InputRotary carries a multi-bit integer value; keep it enabled
799 // (first arg true) and update the numeric output.
800 if (auto *input = qobject_cast<InputRotary *>(elm)) {
801 input->setOn(true, newValue);
802 }
803 } else {
804 // Regular binary inputs treat any non-zero value as "on".
805 if (auto *input = qobject_cast<GraphicElementInput *>(elm)) {
806 input->setOn(static_cast<bool>(newValue));
807 }
808 }
809 }
810
811 apply();
812}
813
814void ElementEditor::inputLocked(const bool value)
815{
816 if (m_elements.isEmpty() || !isEnabled()) {
817 return;
818 }
819
820 for (auto *elm : std::as_const(m_elements)) {
821 if (auto *input = qobject_cast<GraphicElementInput *>(elm)) {
822 input->setLocked(value);
823 }
824 }
825
826 qCDebug(zero) << "Input locked.";
827 apply();
828}
829
830void ElementEditor::triggerChanged(const QString &cmd)
831{
832 sentryBreadcrumb("ui", QStringLiteral("Trigger key: %1").arg(cmd));
833 m_ui->lineEditTrigger->setText(cmd.toUpper());
834 apply();
835}
836
838{
839 sentryBreadcrumb("ui", QStringLiteral("Truth table editor"));
840 if (!m_caps.hasTruthTable || m_elements.isEmpty()) return;
841
842 // Only a single TruthTable element is supported at a time.
843 auto *truthtable = qobject_cast<TruthTable *>(m_elements[0]);
844 if (!truthtable) {
845 return;
846 }
847
848 // --- Build / refresh table ---
849 int nInputs = truthtable->inputSize();
850 int nOutputs = truthtable->outputSize();
851
852 // Columns: nInputs input columns (A, B, C, …) + nOutputs output columns (S0, S1, …).
853 QStringList inputLabels;
854 m_table->setColumnCount(nInputs + nOutputs);
855 // Rows: one per input combination — 2^nInputs.
856 m_table->setRowCount(static_cast<int>(std::pow(2, nInputs)));
857
858 for (int i = 0; i < nInputs; ++i) {
859 inputLabels.append(QChar::fromLatin1(static_cast<char>('A' + i)));
860 }
861
862 for (int i = 0; i < truthtable->outputSize(); ++i) {
863 inputLabels.append("S" + QString::number(i));
864 m_table->setColumnWidth(nInputs + i, 14);
865 }
866
867 m_table->setHorizontalHeaderLabels(inputLabels);
868
869 for (int i = 0; i < std::pow(2, nInputs); ++i) {
870 for (int j = 0; j < nInputs; ++j) {
871 m_table->setColumnWidth(j, 14);
872 // Convert row index to binary string, zero-padded to nInputs digits.
873 auto newItemValue = QString::number(i, 2);
874
875 if (newItemValue.size() < nInputs) {
876 newItemValue = newItemValue.rightJustified(nInputs, '0');
877 }
878
879 // Reuse existing items to avoid re-allocating on every refresh.
880 if (m_table->item(i, j) == nullptr) {
881 auto *newItem = new QTableWidgetItem(newItemValue.at(j), QTableWidgetItem::Type);
882 newItem->setTextAlignment(Qt::AlignCenter);
883 m_table->setItem(i, j, newItem);
884 // Input columns are read-only; only output cells can be toggled.
885 m_table->item(i, j)->setFlags(Qt::ItemIsEnabled);
886 }
887
888 m_table->item(i, j)->setText(newItemValue.at(j));
889 }
890
891 // The key QBitArray stores all outputs interleaved: output z at row i
892 // lives at index 256*z + i (256 rows max per output).
893 auto bitArray = truthtable->key();
894
895 for (int z = 0; z < nOutputs; ++z) {
896 // The key QBitArray stores all outputs interleaved: output z at row i
897 // lives at index 256*z + i (256 rows max per output).
898 const int output = bitArray.at(256 * z + i);
899
900 if (m_table->item(i, nInputs + z) == nullptr) {
901 auto *newOutItem = new QTableWidgetItem(QString(QChar::fromLatin1(static_cast<char>('0' + output))));
902 newOutItem->setTextAlignment(Qt::AlignCenter);
903 m_table->setItem(i, nInputs + z, newOutItem);
904 m_table->item(i, nInputs + z)->setFlags(Qt::ItemIsEnabled);
905 }
906
907 m_table->item(i, nInputs + z)->setText(QString::number(output));
908 }
909 }
910
911 m_tableBox->show();
912}
913
914void ElementEditor::setTruthTableProposition(const int row, const int column)
915{
916 // size() != 1 also rejects an empty selection — `> 1` passed for empty
917 // and then indexed m_elements[0].
918 if (m_elements.size() != 1) return;
919
920 // Input columns (< nInputs) are read-only; only output columns are editable.
921 if (column < m_elements[0]->inputSize()) return;
922
923 // Toggle the cell text immediately for visual feedback before the command
924 // propagates through the undo stack.
925 auto cellItem = m_table->item(row, column);
926 cellItem->setText((cellItem->text() == "0") ? "1" : "0");
927
928 // Compute the flat bit index using the same layout as truthTable(): 256*outputCol + row.
929 auto truthtable = m_elements[0];
930 const int nInputs = truthtable->inputSize();
931 // Compute the flat bit index using the same layout as truthTable(): 256*outputCol + row.
932 const int positionToChange = 256 * (column - nInputs) + row;
933
934 emit sendCommand(new ToggleTruthTableOutputCommand(truthtable, positionToChange, m_scene));
935
936 // Re-render the table so the model state and displayed text stay in sync.
938
939 update();
940
941 m_scene->setCircuitUpdateRequired();
942}
943
945{
946 sentryBreadcrumb("ui", QStringLiteral("Audio file dialog"));
947 if (m_elements.isEmpty()) {
948 return;
949 }
950 auto *audiobox = qobject_cast<AudioBox *>(m_elements[0]);
951 if (!audiobox) {
952 return;
953 }
954
955 const QString filePath = FileDialogs::provider()->getOpenFileName(this, tr("Select any audio"),
956 QString(), tr("Audio") + " (*.mp3 *.mp4 *.wav *.ogg)");
957
958 if (filePath.isEmpty()) {
959 return;
960 }
961
962 audiobox->setAudio(filePath);
963 m_ui->lineCurrentAudioBox->setText(QFileInfo(filePath).fileName());
964}
965
966void ElementEditor::defaultAppearance()
967{
968 sentryBreadcrumb("ui", QStringLiteral("Element appearance reset"));
969 // Set flags before calling apply() so apply() sees them during the loop
970 // and passes true/empty to elm->setAppearance() for each selected element.
971 m_isUpdatingAppearance = true;
972 m_isDefaultAppearance = true;
973 apply();
974}
975
977{
978 // Re-run setCurrentElements on the existing selection to refresh all
979 // visibility flags and widget values after an external state change.
980 setCurrentElements(m_elements);
981}
982
984{
985 // Tweak the group box border colour to match the active theme.
986 // Light theme: rgb(216,216,216) — a light grey; Dark: rgb(66,66,66) — a dark grey.
987 const QString borderColor = (ThemeManager::effectiveTheme() == Theme::Light) ? "216" : "66";
988 const QString styleSheet =
989 "QGroupBox {"
990 " border: 1px solid rgb(%1,%1,%1);"
991 " border-radius: 2px;"
992 " margin-top: 8px;"
993 "}"
994 ""
995 "QGroupBox::title {"
996 " subcontrol-origin: margin;"
997 " subcontrol-position: top;"
998 "}";
999
1000 m_ui->groupBox->setStyleSheet(styleSheet.arg(borderColor));
1001}
Graphic element for the AudioBox (external audio file player) output.
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
#define qCDebug(category)
Definition Common.h:29
Connection: a wire that connects an output port to an input port in the circuit scene.
ElementContextMenu free function for building the element right-click menu.
ElementEditorUi: hand-written UI class for the ElementEditor widget.
static void setSection(bool show, QWidget *label, QWidget *widget)
static bool prepareCombo(QComboBox *cb, bool hasFeature, bool hasSame, const QString &many)
ElementEditor widget for inspecting and modifying selected circuit element properties.
Singleton factory for all circuit element types.
ElementTabNavigator event filter for Tab/Backtab cycling between scene elements.
Enums::WirelessMode WirelessMode
Definition Enums.h:109
Abstract file dialog interface for testability.
IC definition registry with file watching and embedded blob storage.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
Graphic element for the rotary switch input.
LabeledSlider: QSlider subclass that draws numeric labels beneath each tick mark.
Graphic element for a wire junction node.
Main circuit editing scene with undo/redo and user interaction.
SelectionCapabilities computeCapabilities(const QList< GraphicElement * > &elements)
Computes the SelectionCapabilities for a list of selected elements.
SelectionCapabilities struct and computeCapabilities() free function.
Lightweight Sentry helpers gated behind HAVE_SENTRY.
void sentryBreadcrumb(const char *category, const QString &message)
Deserialization/serialization context structs passed through load()/save() call chains.
Circuit and waveform file serialization/deserialization utilities.
Theme management types and singleton ThemeManager.
Graphic element for a user-programmable truth table.
void setScene(Scene *scene)
Associates the editor with scene to read/write selected elements.
void audioBox()
Opens the audio file selector for the selected element(s).
void update()
Refreshes the editor UI to reflect the current selection.
void sendCommand(QUndoCommand *cmd)
Emitted when a property change should be pushed onto the undo stack.
~ElementEditor() override
void fillColorComboBox()
Repopulates the color combo box with available color options.
void contextMenu(QPoint screenPos, QGraphicsItem *itemAtMouse)
Shows the element context menu at screenPos for the item under itemAtMouse.
void updateElementAppearance()
Applies the chosen appearance to the selected element(s).
void truthTable()
Opens the truth table editor for the selected element(s).
void renameAction()
Opens an inline editor to rename the selected element(s).
ElementEditor(QWidget *parent=nullptr)
Constructs the element editor with optional parent.
void updateTheme()
Applies the current theme colors to the editor widgets.
friend class ElementTabNavigator
void changeTriggerAction()
Opens a dialog to change the trigger key for the selected element(s).
void retranslateUi()
Re-applies translatable strings to all editor widgets.
static QString typeToTitleText(const ElementType type)
Returns the display title string for type.
virtual QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
Abstract base class for all graphical circuit elements in wiRedPanda.
virtual void setFrequency(const double freq)
Sets the clock frequency to freq (overridden by clock elements).
ElementType elementType() const
Returns the type identifier for this element.
virtual void setColor(const QString &color)
Sets the element color to color and refreshes the pixmap.
virtual void setAudio(const QString &audio)
Sets the audio file associated with this element to audio.
QList< PropertyDescriptor > editableProperties() const
Returns the list of editable properties this element exposes in the ElementEditor.
void setTrigger(const QKeySequence &trigger)
Sets the keyboard shortcut to trigger and updates the label.
void setLabel(const QString &label)
Sets the label text to label and refreshes the display.
virtual void save(QDataStream &stream, SerializationOptions options) const
virtual void setDelay(const double delay)
Sets the clock phase delay to delay (overridden by clock elements).
virtual void setAppearance(const bool defaultAppearance, const QString &fileName)
Switches the element's appearance.
virtual void setVolume(float vol)
Sets the audio playback volume to vol (0.0–1.0).
const QList< Connection * > & connections() const
Returns the list of wires attached to this port.
Definition Port.cpp:57
Main circuit editing scene.
Definition Scene.h:56
void truthTableElementChanged(GraphicElement *element)
Emitted after a TruthTable element's output bit is toggled (redo or undo).
void contextMenuPos(QPoint screenPos, QGraphicsItem *itemAtMouse)
Emitted when a context menu should appear.
void setCircuitUpdateRequired()
Marks the simulation mapping as stale so it is rebuilt on the next tick.
Definition Scene.cpp:298
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
static void writePandaHeader(QDataStream &stream)
Writes the .panda circuit file header to stream.
static Theme effectiveTheme()
Returns the effective theme (Light or Dark), resolving System to the OS preference.
Undo command that toggles one output bit in a TruthTable element.
Definition Commands.h:417
Undo command for property changes (label, color, frequency, appearance, etc.).
Definition Commands.h:193
void exec(QPoint screenPos, QGraphicsItem *itemAtMouse, const SelectionCapabilities &caps, const QList< GraphicElement * > &elements, QComboBox *colorCombo, Scene *scene, const std::function< void(QUndoCommand *)> &sendCommand, const std::function< void()> &onRename, const std::function< void()> &onTriggerChange, const std::function< void()> &onAppearanceChange, const std::function< void()> &onAppearanceRevert, const std::function< void()> &onFrequencyFocus, const std::function< void()> &onEditSubcircuit={}, const std::function< void()> &onEmbedSubcircuit={}, const std::function< void()> &onExtractToFile={})
Builds and runs the right-click context menu for a set of selected elements.
FileDialogProvider * provider()
Returns the active provider. Never null.
Type
Identifies which property this descriptor represents.
@ Frequency
Clock oscillation frequency in Hz.
@ Delay
Phase delay as fraction of the clock period.
@ Appearance
Custom pixmap appearance selection.
@ Label
User-visible text label.
@ Audio
Audio note selection (buzzer tone).
@ WirelessModeSelector
Wireless routing mode (None / Tx / Rx) — Node elements only.
@ Volume
Audio playback volume (AudioBox, Buzzer).
@ Color
Color selection (LEDs, displays).
@ AudioBox
AudioBox file selection dialog.
@ Trigger
Keyboard trigger shortcut.
@ TruthTable
Editable truth table dialog.