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")); // LCOV_EXCL_LINE — the tile grid (widgetAppearanceStates) is only ever made visible for a single-element selection (see applyCapabilitiesToUi()'s `m_elements.size() == 1` guard), so entering this branch structurally guarantees m_elements.size() == 1, making needsMacro permanently false.
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(); // LCOV_EXCL_LINE — see the matching beginMacro() exclusion above: needsMacro is permanently false here.
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()); // LCOV_EXCL_LINE — c.hasAudio is always false: no element type currently sets hasAudio=true in its ElementInfo (see App/Scene/Commands.cpp's identical exclusion), so prepareCombo() never returns true here.
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 }
585 // LCOV_EXCL_START — no element type currently sets hasAudio=true in its ElementInfo (see
586 // App/Scene/Commands.cpp's identical exclusion), so GraphicElement::editableProperties()
587 // never yields Type::Audio and this case is never dispatched.
589 if (m_ui->comboBoxAudio->currentText() != m_manyAudios) {
590 elm->setAudio(m_ui->comboBoxAudio->currentText());
591 }
592 break;
593 // LCOV_EXCL_STOP
595 // Force uppercase so the stored key sequence matches Qt's key names and is
596 // compatible across keyboard layouts.
597 if (m_ui->lineEditTrigger->text() != m_manyTriggers && m_ui->lineEditTrigger->text().size() <= 1) {
598 elm->setTrigger(QKeySequence(m_ui->lineEditTrigger->text()));
599 }
600 break;
602 if (m_isUpdatingAppearance) {
603 elm->setAppearance(m_isDefaultAppearance, m_appearanceName);
604 }
605 break;
607 if (m_ui->comboBoxWirelessMode->currentText() != m_manyWirelessModes) {
608 if (auto *node = qobject_cast<Node *>(elm)) {
609 node->setWirelessMode(static_cast<WirelessMode>(m_ui->comboBoxWirelessMode->currentIndex()));
610 }
611 }
612 break;
614 elm->setVolume(static_cast<float>(m_ui->sliderVolume->value()) / 100.0f);
615 break;
618 // Managed by their own dialogs (audioBox() / truthTable()); not applied here.
619 break;
620 }
621}
622
623void ElementEditor::apply()
624{
625 sentryBreadcrumb("ui", QStringLiteral("Element properties applied"));
626 qCDebug(three) << "Apply.";
627
628 // m_scene can be null while m_elements still holds the previous tab's
629 // selection (setScene(nullptr) on tab change) — applying would dereference
630 // the null scene below.
631 if (m_elements.isEmpty() || !isEnabled() || !m_scene) {
632 return;
633 }
634
635 // Reject label changes that would create a duplicate wireless Tx channel.
636 // A Tx node's label is its channel name — two Tx nodes on the same label
637 // means one silently receives no signal, which is always a user error.
638 if (m_scene && m_caps.hasWirelessMode) {
639 const QString newLabel = m_ui->lineEditElementLabel->text();
640 const bool labelChanging = (newLabel != m_manyLabels);
641 const WirelessMode newMode = static_cast<WirelessMode>(m_ui->comboBoxWirelessMode->currentIndex());
642 const bool modeChanging = (m_ui->comboBoxWirelessMode->currentText() != m_manyWirelessModes);
643
644 for (auto *elm : std::as_const(m_elements)) {
645 auto *node = qobject_cast<Node *>(elm);
646 if (!node) continue;
647
648 const QString candidateLabel = labelChanging ? newLabel : node->label();
649 const WirelessMode candidateMode = modeChanging ? newMode : node->wirelessMode();
650
651 if (candidateMode != WirelessMode::Tx || candidateLabel.isEmpty()) continue;
652
653 for (auto *other : m_scene->elements()) {
654 if (other == elm) continue;
655 auto *otherNode = qobject_cast<Node *>(other);
656 if (!otherNode || otherNode->wirelessMode() != WirelessMode::Tx) continue;
657 if (otherNode->label() == candidateLabel) {
658 QMessageBox::warning(this,
659 tr("Duplicate Wireless Channel"),
660 tr("A Tx node with label \"%1\" already exists.\n"
661 "Each wireless channel must have a unique label.").arg(candidateLabel));
662 update();
663 return;
664 }
665 }
666 }
667 }
668
669 // Collect connections that will be severed by wireless mode changes.
670 // setWirelessMode() hides the replaced port but does not delete connections
671 // itself — that is handled here via DeleteItemsCommand so undo can restore them.
672 QList<QGraphicsItem *> wirelessConnsToDelete;
673 if (m_scene && m_caps.hasWirelessMode) {
674 const WirelessMode newMode = static_cast<WirelessMode>(m_ui->comboBoxWirelessMode->currentIndex());
675 const bool modeChanging = (m_ui->comboBoxWirelessMode->currentText() != m_manyWirelessModes);
676
677 if (modeChanging) {
678 for (auto *elm : std::as_const(m_elements)) {
679 auto *node = qobject_cast<Node *>(elm);
680 if (!node) continue;
681
682 Port *port = (newMode == WirelessMode::Rx) ? static_cast<Port *>(node->inputPort())
683 : (newMode == WirelessMode::Tx) ? static_cast<Port *>(node->outputPort())
684 : nullptr;
685 if (port) {
686 for (auto *conn : port->connections()) {
687 wirelessConnsToDelete.append(static_cast<QGraphicsItem *>(conn));
688 }
689 }
690 }
691 }
692 }
693
694 // Snapshot current state into oldData before modifying anything.
695 // UpdateCommand uses oldData for undo and captures the post-modification
696 // state internally as newData when it is constructed.
697 QByteArray oldData;
698 QDataStream stream(&oldData, QIODevice::WriteOnly);
700
701 for (auto *elm : std::as_const(m_elements)) {
702 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
703 for (const auto &prop : elm->editableProperties()) {
704 applyProperty(elm, prop.type);
705 }
706 }
707
708 // Reset the one-shot appearance update flag after applying to all elements.
709 if (m_isUpdatingAppearance) {
710 m_isUpdatingAppearance = false;
711 }
712
713 // When wireless mode changes sever connections, group the property update
714 // and the delete into a single undo macro so both are undone together.
715 const bool needsMacro = !wirelessConnsToDelete.isEmpty();
716 if (needsMacro) {
717 m_scene->undoStack()->beginMacro(tr("Change wireless mode"));
718 }
719
720 m_scene->receiveCommand(new UpdateCommand(m_elements, oldData, m_scene));
721
722 if (needsMacro) {
723 m_scene->receiveCommand(new DeleteItemsCommand(wirelessConnsToDelete, m_scene));
724 m_scene->undoStack()->endMacro();
725 }
726}
727
728void ElementEditor::blobNameEditingFinished()
729{
730 if (m_elements.isEmpty() || !m_caps.isEmbedded) {
731 return;
732 }
733
734 const QString newBlobName = m_ui->lineEditBlobName->text().trimmed();
735 const QString oldBlobName = m_elements.first()->blobName();
736
737 // editingFinished fires on plain focus-out too, not just on an actual edit — guard against
738 // the multi-selection placeholder as well as an empty/unchanged field so clicking into and
739 // out of the field without typing anything never renames.
740 if (newBlobName.isEmpty() || newBlobName == oldBlobName || newBlobName == m_manyLabels) {
741 return;
742 }
743
744 // Reject a rename that collides with a different, already-registered blob: ICRegistry::
745 // renameBlob() would silently overwrite that other blob's bytes with this IC's circuit,
746 // permanently destroying it with no undo path back. Refuse before pushing anything, mirroring
747 // the duplicate-wireless-Tx-channel guard above.
748 if (m_scene->icRegistry()->hasBlob(newBlobName)) {
749 QMessageBox::warning(this,
750 tr("Duplicate IC Name"),
751 tr("An embedded IC named \"%1\" already exists.\n"
752 "Choose a different name.").arg(newBlobName));
753 return;
754 }
755
756 // Renaming is its own independent, immediately-applied action — pushed standalone rather
757 // than folded into apply()'s UpdateCommand, so undo/redo never has to reconcile a generic
758 // property snapshot against the IC registry's shared blob-name state.
759 m_scene->receiveCommand(new RenameBlobCommand(oldBlobName, newBlobName, m_scene));
760}
761
762void ElementEditor::inputIndexChanged(const int index)
763{
764 if (m_elements.isEmpty() || !isEnabled()) {
765 return;
766 }
767 sentryBreadcrumb("ui", QStringLiteral("Input count: %1").arg(index));
768
769 if (m_caps.canChangeInputSize && (m_ui->comboBoxInputSize->currentText() != m_manyIS)) {
770 emit sendCommand(new ChangePortSizeCommand(m_elements, m_ui->comboBoxInputSize->currentData().toInt(), m_scene, true));
771 }
772
773 qCDebug(zero) << "Input size changed to " << index;
774 update();
775}
776
777void ElementEditor::outputIndexChanged(const int index)
778{
779 if (m_elements.isEmpty() || !isEnabled()) {
780 return;
781 }
782 sentryBreadcrumb("ui", QStringLiteral("Output count: %1").arg(index));
783
784 if (m_caps.canChangeOutputSize && (m_ui->comboBoxOutputSize->currentText() != m_manyOS)) {
785 emit sendCommand(new ChangePortSizeCommand(m_elements, m_ui->comboBoxOutputSize->currentData().toInt(), m_scene, false));
786 }
787
788 qCDebug(zero) << "Output size changed to " << index;
789 update();
790}
791
792void ElementEditor::outputValueChanged(const QString &value)
793{
794 if (m_elements.isEmpty() || !isEnabled()) {
795 return;
796 }
797
798 const int newValue = value.toInt();
799
800 for (auto *elm : std::as_const(m_elements)) {
801 if (elm->elementType() == ElementType::InputRotary) {
802 // InputRotary carries a multi-bit integer value; keep it enabled
803 // (first arg true) and update the numeric output.
804 if (auto *input = qobject_cast<InputRotary *>(elm)) {
805 input->setOn(true, newValue);
806 }
807 } else {
808 // Regular binary inputs treat any non-zero value as "on".
809 if (auto *input = qobject_cast<GraphicElementInput *>(elm)) {
810 input->setOn(static_cast<bool>(newValue));
811 }
812 }
813 }
814
815 apply();
816}
817
818void ElementEditor::inputLocked(const bool value)
819{
820 if (m_elements.isEmpty() || !isEnabled()) {
821 return;
822 }
823
824 for (auto *elm : std::as_const(m_elements)) {
825 if (auto *input = qobject_cast<GraphicElementInput *>(elm)) {
826 input->setLocked(value);
827 }
828 }
829
830 qCDebug(zero) << "Input locked.";
831 apply();
832}
833
834void ElementEditor::triggerChanged(const QString &cmd)
835{
836 sentryBreadcrumb("ui", QStringLiteral("Trigger key: %1").arg(cmd));
837 m_ui->lineEditTrigger->setText(cmd.toUpper());
838 apply();
839}
840
842{
843 sentryBreadcrumb("ui", QStringLiteral("Truth table editor"));
844 if (!m_caps.hasTruthTable || m_elements.isEmpty()) return;
845
846 // Only a single TruthTable element is supported at a time.
847 auto *truthtable = qobject_cast<TruthTable *>(m_elements[0]);
848 if (!truthtable) {
849 return; // LCOV_EXCL_LINE — m_caps.hasTruthTable is AND-merged across the selection and only TruthTable::hasTruthTable() ever returns true, so hasTruthTable implies m_elements[0] already IS a TruthTable.
850 }
851
852 // --- Build / refresh table ---
853 int nInputs = truthtable->inputSize();
854 int nOutputs = truthtable->outputSize();
855
856 // Columns: nInputs input columns (A, B, C, …) + nOutputs output columns (S0, S1, …).
857 QStringList inputLabels;
858 m_table->setColumnCount(nInputs + nOutputs);
859 // Rows: one per input combination — 2^nInputs.
860 m_table->setRowCount(static_cast<int>(std::pow(2, nInputs)));
861
862 for (int i = 0; i < nInputs; ++i) {
863 inputLabels.append(QChar::fromLatin1(static_cast<char>('A' + i)));
864 }
865
866 for (int i = 0; i < truthtable->outputSize(); ++i) {
867 inputLabels.append("S" + QString::number(i));
868 m_table->setColumnWidth(nInputs + i, 14);
869 }
870
871 m_table->setHorizontalHeaderLabels(inputLabels);
872
873 for (int i = 0; i < std::pow(2, nInputs); ++i) {
874 for (int j = 0; j < nInputs; ++j) {
875 m_table->setColumnWidth(j, 14);
876 // Convert row index to binary string, zero-padded to nInputs digits.
877 auto newItemValue = QString::number(i, 2);
878
879 if (newItemValue.size() < nInputs) {
880 newItemValue = newItemValue.rightJustified(nInputs, '0');
881 }
882
883 // Reuse existing items to avoid re-allocating on every refresh.
884 if (m_table->item(i, j) == nullptr) {
885 auto *newItem = new QTableWidgetItem(newItemValue.at(j), QTableWidgetItem::Type);
886 newItem->setTextAlignment(Qt::AlignCenter);
887 m_table->setItem(i, j, newItem);
888 // Input columns are read-only; only output cells can be toggled.
889 m_table->item(i, j)->setFlags(Qt::ItemIsEnabled);
890 }
891
892 m_table->item(i, j)->setText(newItemValue.at(j));
893 }
894
895 // The key QBitArray stores all outputs interleaved: output z at row i
896 // lives at index 256*z + i (256 rows max per output).
897 auto bitArray = truthtable->key();
898
899 for (int z = 0; z < nOutputs; ++z) {
900 // The key QBitArray stores all outputs interleaved: output z at row i
901 // lives at index 256*z + i (256 rows max per output).
902 const int output = bitArray.at(256 * z + i);
903
904 if (m_table->item(i, nInputs + z) == nullptr) {
905 auto *newOutItem = new QTableWidgetItem(QString(QChar::fromLatin1(static_cast<char>('0' + output))));
906 newOutItem->setTextAlignment(Qt::AlignCenter);
907 m_table->setItem(i, nInputs + z, newOutItem);
908 m_table->item(i, nInputs + z)->setFlags(Qt::ItemIsEnabled);
909 }
910
911 m_table->item(i, nInputs + z)->setText(QString::number(output));
912 }
913 }
914
915 m_tableBox->show();
916}
917
918void ElementEditor::setTruthTableProposition(const int row, const int column)
919{
920 // size() != 1 also rejects an empty selection — `> 1` passed for empty
921 // and then indexed m_elements[0].
922 if (m_elements.size() != 1) return;
923
924 // Input columns (< nInputs) are read-only; only output columns are editable.
925 if (column < m_elements[0]->inputSize()) return;
926
927 // Toggle the cell text immediately for visual feedback before the command
928 // propagates through the undo stack.
929 auto cellItem = m_table->item(row, column);
930 cellItem->setText((cellItem->text() == "0") ? "1" : "0");
931
932 // Compute the flat bit index using the same layout as truthTable(): 256*outputCol + row.
933 auto truthtable = m_elements[0];
934 const int nInputs = truthtable->inputSize();
935 // Compute the flat bit index using the same layout as truthTable(): 256*outputCol + row.
936 const int positionToChange = 256 * (column - nInputs) + row;
937
938 emit sendCommand(new ToggleTruthTableOutputCommand(truthtable, positionToChange, m_scene));
939
940 // Re-render the table so the model state and displayed text stay in sync.
942
943 update();
944
945 m_scene->setCircuitUpdateRequired();
946}
947
949{
950 sentryBreadcrumb("ui", QStringLiteral("Audio file dialog"));
951 if (m_elements.isEmpty()) {
952 return;
953 }
954 auto *audiobox = qobject_cast<AudioBox *>(m_elements[0]);
955 if (!audiobox) {
956 return;
957 }
958
959 const QString filePath = FileDialogs::provider()->getOpenFileName(this, tr("Select any audio"),
960 QString(), tr("Audio") + " (*.mp3 *.mp4 *.wav *.ogg)");
961
962 if (filePath.isEmpty()) {
963 return;
964 }
965
966 audiobox->setAudio(filePath);
967 m_ui->lineCurrentAudioBox->setText(QFileInfo(filePath).fileName());
968}
969
970void ElementEditor::defaultAppearance()
971{
972 sentryBreadcrumb("ui", QStringLiteral("Element appearance reset"));
973 // Set flags before calling apply() so apply() sees them during the loop
974 // and passes true/empty to elm->setAppearance() for each selected element.
975 m_isUpdatingAppearance = true;
976 m_isDefaultAppearance = true;
977 apply();
978}
979
981{
982 // Re-run setCurrentElements on the existing selection to refresh all
983 // visibility flags and widget values after an external state change.
984 setCurrentElements(m_elements);
985}
986
988{
989 // Tweak the group box border colour to match the active theme.
990 // Light theme: rgb(216,216,216) — a light grey; Dark: rgb(66,66,66) — a dark grey.
991 const QString borderColor = (ThemeManager::effectiveTheme() == Theme::Light) ? "216" : "66";
992 const QString styleSheet =
993 "QGroupBox {"
994 " border: 1px solid rgb(%1,%1,%1);"
995 " border-radius: 2px;"
996 " margin-top: 8px;"
997 "}"
998 ""
999 "QGroupBox::title {"
1000 " subcontrol-origin: margin;"
1001 " subcontrol-position: top;"
1002 "}";
1003
1004 m_ui->groupBox->setStyleSheet(styleSheet.arg(borderColor));
1005}
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.