wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
BeWavedDolphin.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 <algorithm>
7#include <cmath>
8
9#include <QAbstractItemView>
10#include <QApplication>
11#include <QClipboard>
12#include <QCloseEvent>
13#include <QHeaderView>
14#include <QMessageBox>
15#include <QTableView>
16#include <QTextStream>
17#include <QWheelEvent>
18
30#include "App/Core/Common.h"
32#include "App/Core/Settings.h"
37#include "App/UI/ClockDialog.h"
39#include "App/UI/LengthDialog.h"
40
41BewavedDolphin::BewavedDolphin(Scene *scene, const bool askConnection, DolphinHost *host, QWidget *parent)
42 : QMainWindow(parent)
43 , m_ui(std::make_unique<BewavedDolphinUi>())
44 , m_host(host)
45 , m_externalScene(scene)
46 // askConnection controls whether closing consults checkSave()'s save-changes prompt
47 // (see closeEvent()); the separate save-and-link prompt in associateToWiRedPanda()
48 // is gated on Application::interactiveMode instead.
49 , m_askConnection(askConnection)
50{
51 m_ui->setupUi(this);
52 m_ui->retranslateUi(this);
53
54 // WA_DeleteOnClose ensures the window is freed when closed without the caller
55 // needing to track its lifetime
56 setAttribute(Qt::WA_DeleteOnClose);
57 // Modal so the user cannot interact with the main circuit while the waveform is open
58 setWindowModality(Qt::WindowModal);
59 setWindowTitle(tr("beWavedDolphin Simulator"));
60
61 resize(800, 500);
62
63 restoreGeometry(Settings::dolphinGeometry());
64
65 // The delegate paints the waveform cells; the table is the central widget directly
66 m_delegate = new SignalDelegate(this);
67 m_signalTableView->setItemDelegate(m_delegate);
68
69 // Zoom state + view metrics live in DolphinZoom; the controller keeps the UI glue
70 // (breadcrumbs, action enable-state via zoomChanged(), and the wheel event filter).
71 m_zoom = std::make_unique<DolphinZoom>(m_signalTableView);
72
73 // Native scrollbars let long waveforms scroll; zoom changes row/column metrics instead
74 m_signalTableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
75 m_signalTableView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
76 m_ui->verticalLayout->addWidget(m_signalTableView);
77
78 // The mouse wheel over the table zooms the columns (see eventFilter)
79 m_signalTableView->viewport()->installEventFilter(this);
80
81 m_ui->mainToolBar->setToolButtonStyle(Settings::labelsUnderIcons() ? Qt::ToolButtonTextUnderIcon : Qt::ToolButtonIconOnly);
82
83 connect(m_ui->actionAbout, &QAction::triggered, this, &BewavedDolphin::on_actionAbout_triggered);
84 connect(m_ui->actionAboutQt, &QAction::triggered, this, &BewavedDolphin::on_actionAboutQt_triggered);
85 connect(m_ui->actionClear, &QAction::triggered, this, &BewavedDolphin::on_actionClear_triggered);
86 connect(m_ui->actionCombinational, &QAction::triggered, this, &BewavedDolphin::on_actionCombinational_triggered);
87 connect(m_ui->actionCopy, &QAction::triggered, this, &BewavedDolphin::on_actionCopy_triggered);
88 connect(m_ui->actionCut, &QAction::triggered, this, &BewavedDolphin::on_actionCut_triggered);
89 connect(m_ui->actionExit, &QAction::triggered, this, &BewavedDolphin::on_actionExit_triggered);
90 connect(m_ui->actionExportToPdf, &QAction::triggered, this, &BewavedDolphin::on_actionExportToPdf_triggered);
91 connect(m_ui->actionExportToPng, &QAction::triggered, this, &BewavedDolphin::on_actionExportToPng_triggered);
92 connect(m_ui->actionFitScreen, &QAction::triggered, this, &BewavedDolphin::on_actionFitScreen_triggered);
93 connect(m_ui->actionInvert, &QAction::triggered, this, &BewavedDolphin::on_actionInvert_triggered);
94 connect(m_ui->actionLoad, &QAction::triggered, this, &BewavedDolphin::on_actionLoad_triggered);
95 connect(m_ui->actionPaste, &QAction::triggered, this, &BewavedDolphin::on_actionPaste_triggered);
96 connect(m_ui->actionResetZoom, &QAction::triggered, this, &BewavedDolphin::on_actionResetZoom_triggered);
97 connect(m_ui->actionSave, &QAction::triggered, this, &BewavedDolphin::on_actionSave_triggered);
98 connect(m_ui->actionSaveAs, &QAction::triggered, this, &BewavedDolphin::on_actionSaveAs_triggered);
99 connect(m_ui->actionSetClockWave, &QAction::triggered, this, &BewavedDolphin::on_actionSetClockWave_triggered);
100 connect(m_ui->actionSetLength, &QAction::triggered, this, &BewavedDolphin::on_actionSetLength_triggered);
101 connect(m_ui->actionSetTo0, &QAction::triggered, this, &BewavedDolphin::on_actionSetTo0_triggered);
102 connect(m_ui->actionSetTo1, &QAction::triggered, this, &BewavedDolphin::on_actionSetTo1_triggered);
103 connect(m_ui->actionShowNumbers, &QAction::triggered, this, &BewavedDolphin::on_actionShowNumbers_triggered);
104 connect(m_ui->actionShowWaveforms, &QAction::triggered, this, &BewavedDolphin::on_actionShowWaveforms_triggered);
105 connect(m_ui->actionZoomIn, &QAction::triggered, this, &BewavedDolphin::on_actionZoomIn_triggered);
106 connect(m_ui->actionZoomOut, &QAction::triggered, this, &BewavedDolphin::on_actionZoomOut_triggered);
107 connect(m_ui->actionAutoCrop, &QAction::triggered, this, &BewavedDolphin::on_actionAutoCrop_triggered);
108
109 // Undo/redo for waveform cell edits (#19) -- mirrors Scene's m_undoAction/m_redoAction wiring.
110 connect(&m_undoStack, &QUndoStack::canUndoChanged, m_ui->actionUndo, &QAction::setEnabled);
111 connect(&m_undoStack, &QUndoStack::canRedoChanged, m_ui->actionRedo, &QAction::setEnabled);
112 connect(m_ui->actionUndo, &QAction::triggered, &m_undoStack, &QUndoStack::undo);
113 connect(m_ui->actionRedo, &QAction::triggered, &m_undoStack, &QUndoStack::redo);
114}
115
120
121void BewavedDolphin::createWaveform(const QString &fileName)
122{
123 prepare(fileName);
124
125 if (fileName.isEmpty()) {
126 // No saved waveform — start with all-zero inputs and run once to populate outputs
127 setWindowTitle(tr("beWavedDolphin Simulator"));
128 run();
129 } else {
130 // Try the stored path as-is first (handles an absolute path); if that doesn't
131 // resolve, fall back to just the filename relative to the main window's working
132 // directory so that relative paths stored inside .panda files still resolve.
133 QFileInfo fileInfo(fileName);
134 if (fileInfo.isRelative() || !fileInfo.exists()) {
135 fileInfo.setFile(m_host->currentDir(), QFileInfo(fileName).fileName());
136 }
137
138 if (!fileInfo.exists()) {
139 m_ui->statusbar->showMessage(tr("File \"%1\" does not exist!").arg(fileName), 4000);
140 return;
141 }
142
143 load(fileInfo.absoluteFilePath());
144 }
145
146 qCDebug(zero) << "Resuming digital circuit main window after waveform simulation is finished.";
147 // Reset edit flag — loading a file or a fresh run does not constitute a user edit
148 m_edited = false;
149 // loadNewTable()'s initial clear (and, on the file-load branch, applyWaveformData()) may
150 // have pushed/undone entries as part of setting up the document; undo history should start
151 // clean from here, not from whatever internal setup happened to do.
152 m_undoStack.clear();
153}
154
156{
157 prepare();
158 loadFromTerminal();
159 m_undoStack.clear();
160}
161
162void BewavedDolphin::loadFromTerminal()
163{
164 // DolphinFile owns the stdin protocol parsing; applyWaveformData() applies the parsed
165 // rows the same way a file load does (setLength + fill inputs + run).
166 QTextStream cin(stdin);
167 applyWaveformData(DolphinFile::parseTerminal(cin, m_inputPorts));
168}
169
170void BewavedDolphin::prepare(const QString &fileName)
171{
172 qCDebug(zero) << "Updating window name with current: " << fileName;
173 m_simulation = m_externalScene->simulation();
174 // Construct the sweep driver before loadNewTable(), whose initial clear triggers run().
175 m_simDriver = std::make_unique<WaveformSimulator>(m_externalScene, m_simulation);
176
177 qCDebug(zero) << "Collecting and ordering the scene's input/output elements.";
178 const auto tableSignals = DolphinModelBuilder::collect(m_externalScene);
179 m_inputs = tableSignals.inputs;
180 m_outputs = tableSignals.outputs;
181 m_inputPorts = tableSignals.inputPorts;
182
183 qCDebug(zero) << "Loading initial data into the table.";
184 loadNewTable(tableSignals.inputLabels, tableSignals.outputLabels);
185}
186
187void BewavedDolphin::loadNewTable(const QStringList &inputLabels, const QStringList &outputLabels)
188{
189 qCDebug(zero) << "Snapshotting current input values into oldvalues, to restore after simulation.";
190 // Snapshot the live input port states before the simulation sweep overwrites them, so
191 // they can be restored after the sweep completes.
192 m_oldInputValues = WaveformSimulator::captureInputs(m_inputs, m_inputPorts);
193
194 qCDebug(zero) << "Num iter = " << m_length;
195
196 // Rows = total signals (inputs + outputs); columns = simulation length in time steps.
197 // loadNewTable() runs exactly once per window (each is WA_DeleteOnClose), so this is a
198 // single allocation; parenting to `this` ties its lifetime to the window — not re-created.
199 m_model = new SignalModel(static_cast<int>(inputLabels.size() + outputLabels.size()), m_length, this);
200 m_signalTableView->setModel(m_model);
201
202 // Input rows come first, then output rows — the split point is inputLabels.size()
203 m_model->setVerticalHeaderLabels(inputLabels + outputLabels);
204 m_model->setInputRows(static_cast<int>(inputLabels.size()));
205
206 // The delegate derives each cell's waveform from the model; it owns the current mode.
207
208 // A cell's rising/falling edge depends on its left neighbour, so any value change must
209 // repaint the whole visible grid (update() coalesces, so a full sweep is one repaint).
210 connect(m_model, &QAbstractItemModel::dataChanged, this, [this] {
211 m_signalTableView->viewport()->update();
212 });
213
214 m_signalTableView->setAlternatingRowColors(true);
215 m_signalTableView->setShowGrid(false);
216
217 // Fixed section sizes keep waveforms aligned; applyZoom() drives the actual
218 // row/column sizes from the current zoom factor.
219 m_signalTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Fixed);
220 m_signalTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Fixed);
221
222 qCDebug(zero) << "Inputs: " << inputLabels.size() << ", outputs: " << outputLabels.size();
223
224 // Initialise all input cells to 0 and compute the first output sweep
225 on_actionClear_triggered();
226
227 connect(m_signalTableView, &QAbstractItemView::doubleClicked, this, &BewavedDolphin::on_tableView_cellDoubleClicked);
228 connect(m_signalTableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BewavedDolphin::on_tableView_selectionChanged);
229
230 // Size rows/columns/font for the current zoom so the table is correct even
231 // before the window is shown (e.g. in headless tests and offscreen export).
232 applyZoom();
233}
234
235void BewavedDolphin::on_tableView_cellDoubleClicked()
236{
237 // Toggle each selected cell between 0 and 1 -- identical to Invert, so route through the
238 // same undoable helper rather than duplicating the DolphinEdits call inline.
239 applyToSelectedCells([](int v) { return (v + 1) % 2; });
240}
241
242void BewavedDolphin::on_tableView_selectionChanged()
243{
244 m_externalScene->clearSelection();
245
246 const auto indexes = m_signalTableView->selectionModel()->selectedIndexes();
247
248 m_ui->actionSetClockWave->setEnabled(!indexes.isEmpty());
249
250 // Highlight the corresponding input element in the circuit editor when the user
251 // selects a waveform row, giving visual feedback about which signal they are editing.
252 // Output rows (index >= m_inputs.size()) have no element to highlight.
253 for (auto &index : indexes) {
254 if (index.row() < m_inputs.size()) {
255 m_inputs.at(index.row())->setSelected(true);
256 }
257 }
258
259 m_externalScene->view()->update();
260}
261
262bool BewavedDolphin::elementsStillLive() const
263{
264 const auto sceneElements = m_externalScene->elements();
265 for (auto *elm : std::as_const(m_inputs)) {
266 if (!sceneElements.contains(elm)) {
267 return false;
268 }
269 }
270 for (auto *elm : std::as_const(m_outputs)) {
271 if (!sceneElements.contains(elm)) {
272 return false;
273 }
274 }
275 return true;
276}
277
279{
280 // Guard against a caller invoking run() before prepare()/loadNewTable() has built
281 // the sweep driver and table model (every current call site prepares first, but
282 // this is a public entry point with no compile-time guarantee of that ordering).
283 if (!m_simDriver || !m_model) {
284 return;
285 }
286
287 // An input/output element this waveform was built from may have been deleted from the
288 // live scene since prepare() ran — via the main canvas, or an MCP client, neither of
289 // which this window's modality blocks (MCP mutates the scene directly, bypassing Qt
290 // input routing entirely). Skip the sweep rather than dereferencing freed elements.
291 if (!elementsStillLive()) {
292 return;
293 }
294
295 // Drive the circuit across every time column. Inputs are read from the model and the
296 // computed outputs (isInput=false → green; changeNext=false → caller refreshes) are
297 // written back, then the original input states are restored so the live simulation
298 // resumes correctly.
299 {
300 SignalModel::BulkEditGuard guard(*m_model);
301 m_simDriver->sweep(
302 m_inputs, m_outputs, m_inputPorts, m_model->columnCount(),
303 [this](int row, int col) { return m_model->value(row, col) != 0; },
304 [this](int row, int col, int value) { m_model->setValue(row, col, value); });
305 }
306
307 qCDebug(three) << "Setting inputs back to old values.";
308 WaveformSimulator::restoreInputs(m_inputs, m_oldInputValues);
309}
310
311bool BewavedDolphin::eventFilter(QObject *watched, QEvent *event)
312{
313 // The mouse wheel zooms the columns (the original beWavedDolphin behavior): any
314 // wheel over the table viewport widens/narrows the columns and is consumed, so the
315 // table never scrolls on wheel. Use the scrollbar to pan a long waveform.
316 if (watched == m_signalTableView->viewport() && event->type() == QEvent::Wheel) {
317 auto *wheel = static_cast<QWheelEvent *>(event);
318 if (wheel->angleDelta().y() > 0) {
319 on_actionZoomIn_triggered();
320 } else if (wheel->angleDelta().y() < 0) {
321 on_actionZoomOut_triggered();
322 }
323 return true;
324 }
325
326 return QMainWindow::eventFilter(watched, event);
327}
328
329void BewavedDolphin::applyZoom()
330{
331 // The zoom math lives in DolphinZoom; the controller drives the action enable-state.
332 m_zoom->apply();
333 zoomChanged();
334}
335
336void BewavedDolphin::on_actionExit_triggered()
337{
338 Application::guardedSlot(this, [this] {
339 close();
340 });
341}
342
343void BewavedDolphin::closeEvent(QCloseEvent *event)
344{
345 // askConnection gates whether closing consults checkSave() at all — with it false, the
346 // window must still be closable, just without the save-changes prompt.
347 if (m_askConnection && !checkSave()) {
348 event->ignore();
349 return;
350 }
351
352 event->accept();
353}
354
355void BewavedDolphin::resizeEvent(QResizeEvent *event)
356{
357 QMainWindow::resizeEvent(event);
358 if (m_exerciseOverlay && m_exerciseOverlay->isVisible()) {
359 m_exerciseOverlay->repositionToParent();
360 }
361}
362
364{
365 m_exerciseOverlay = overlay;
366}
367
369{
370 return m_ui->mainToolBar;
371}
372
374{
375 return m_ui->actionCombinational;
376}
377
379{
380 on_actionCombinational_triggered();
381}
382
383bool BewavedDolphin::checkSave()
384{
385 if (!m_edited) {
386 return true;
387 }
388
389 auto reply =
390 QMessageBox::question(
391 this,
392 tr("wiRedPanda - beWavedDolphin"),
393 tr("Save simulation before closing?"),
394 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
395
396 switch (reply) {
397 // After saving, check m_edited again: if the save itself failed (e.g. user dismissed
398 // the Save As dialog), m_edited stays true and we should not allow the close
399 case QMessageBox::Save: on_actionSave_triggered(); return (!m_edited);
400 case QMessageBox::Discard: return true;
401 case QMessageBox::Cancel: return false;
402 default: return true;
403 }
404}
405
406void BewavedDolphin::setCellValue(const int row, const int col, const int value)
407{
408 // The model stores only the logic value; the delegate derives the waveform segment
409 // (from this cell and its left neighbour) and the input/output colour at paint time.
410 m_model->setValue(row, col, value);
411}
412
413int BewavedDolphin::inputRow(const QString &label) const
414{
415 for (int row = 0; row < m_inputs.size(); ++row) {
416 if (m_inputs.at(row)->label() == label) {
417 return row;
418 }
419 }
420
421 return -1;
422}
423
425{
426 // Read the computed waveform into a plain DTO so automation consumers (the MCP server)
427 // need neither the live model nor the element vectors. Input rows come first; output
428 // rows follow at the m_inputs.size() offset (mirroring the table's row layout).
429 WaveformSnapshot result;
430
431 for (int row = 0; row < m_inputs.size(); ++row) {
432 Signal signal;
433 signal.label = m_inputs.at(row)->label();
434 for (int col = 0; col < duration; ++col) {
435 signal.values.append(m_model->value(row, col));
436 }
437 result.inputs.append(signal);
438 }
439
440 for (int row = 0; row < m_outputs.size(); ++row) {
441 Signal signal;
442 signal.label = m_outputs.at(row)->label();
443 const int outputRowIndex = static_cast<int>(m_inputs.size()) + row;
444 for (int col = 0; col < duration; ++col) {
445 signal.values.append(m_model->value(outputRowIndex, col));
446 }
447 result.outputs.append(signal);
448 }
449
450 return result;
451}
452
454{
455 QMainWindow::show();
456 applyZoom();
457}
458
460{
461 if (!m_model) {
462 return;
463 }
464 // Outputs in the same CSV format used by loadFromTerminal() / the CSV save path,
465 // allowing round-trip scripted use without a GUI.
466 QTextStream(stdout) << DolphinExporter::csvText(m_model);
467}
468
469void BewavedDolphin::saveToTxt(QTextStream &stream)
470{
471 if (!m_model) {
472 return;
473 }
474
475 // Same staleness hazard as run() (see elementsStillLive()); saveToTxt() dereferences
476 // m_inputs/m_outputs via sweep() below too. Throw instead of silently no-op'ing so an
477 // MCP client (the only realistic caller once run() already guards interactive use) gets
478 // a clean error instead of a "successful" empty/truncated export.
479 if (!elementsStillLive()) {
480 throw PANDACEPTION("Cannot export: the circuit this waveform was built from has changed.");
481 }
482
483 // Dump the full combinational truth table. Build it in a throwaway model (like
484 // renderWaveform() uses a throwaway view) so the live document is never mutated —
485 // exporting must not clobber the user's waveform (e.g. an MCP persistent session).
486 // Clamp to SignalModel::kMaxColumns, exactly like on_actionCombinational_triggered's
487 // identical cap below — otherwise a circuit with ~25+ effective input ports allocates
488 // tens of millions of cells, and m_inputPorts >= 31 overflows the int cast (UB).
489 const int columns = static_cast<int>((std::min)(static_cast<double>(SignalModel::kMaxColumns), std::pow(2, m_inputPorts)));
490 SignalModel truthTable(m_model->rowCount(), columns);
491
492 // Carry the live model's row labels onto the throwaway model so the exporter, which
493 // reads labels and values from a single model, formats the same headers as before.
494 QStringList labels;
495 labels.reserve(m_model->rowCount());
496 for (int row = 0; row < m_model->rowCount(); ++row) {
497 labels.append(m_model->verticalHeaderItem(row)->text());
498 }
499 truthTable.setVerticalHeaderLabels(labels);
500
501 DolphinEdits::combinational(truthTable, m_inputPorts, columns);
502
503 // Compute the outputs for every input combination, then restore the live inputs the
504 // sweep perturbed (the same capture/restore contract run() relies on).
505 {
506 SignalModel::BulkEditGuard guard(truthTable);
507 m_simDriver->sweep(
508 m_inputs, m_outputs, m_inputPorts, columns,
509 [&truthTable](int row, int col) { return truthTable.value(row, col) != 0; },
510 [&truthTable](int row, int col, int value) { truthTable.setValue(row, col, value); });
511 }
512 WaveformSimulator::restoreInputs(m_inputs, m_oldInputValues);
513
514 DolphinExporter::writeTruthTableText(stream, &truthTable, static_cast<int>(m_inputs.size()));
515}
516
517bool BewavedDolphin::exportToPng(const QString &filename)
518{
519 // Public façade for the MCP server: swallow exceptions and report success as a bool.
520 try {
521 return DolphinExporter::exportToPng(m_model, m_delegate->plotType(), filename);
522 } catch (...) {
523 return false;
524 }
525}
526
527std::pair<QList<QPair<int, int>>, QList<int>> BewavedDolphin::snapshotCells(const QModelIndexList &indexes) const
528{
529 QList<QPair<int, int>> cells;
530 QList<int> oldValues;
531 cells.reserve(indexes.size());
532 oldValues.reserve(indexes.size());
533 for (const auto &idx : indexes) {
534 cells.append({idx.row(), idx.column()});
535 oldValues.append(m_model->value(idx.row(), idx.column()));
536 }
537 return {cells, oldValues};
538}
539
540QModelIndexList BewavedDolphin::allCellIndexes(int rows, int cols) const
541{
542 QModelIndexList indexes;
543 indexes.reserve(rows * cols);
544 for (int row = 0; row < rows; ++row) {
545 for (int col = 0; col < cols; ++col) {
546 indexes.append(m_model->index(row, col));
547 }
548 }
549 return indexes;
550}
551
552void BewavedDolphin::applyToSelectedCells(const std::function<int(int)> &valueFn)
553{
554 const auto indexes = m_signalTableView->selectionModel()->selectedIndexes();
555 const auto [cells, oldValues] = snapshotCells(indexes);
556
557 DolphinEdits::applyToCells(*m_model, indexes, valueFn);
558
559 qCDebug(zero) << "Running simulation.";
560 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
561 m_edited = true;
562 run();
563 }));
564}
565
566void BewavedDolphin::on_actionSetTo0_triggered()
567{
568 Application::guardedSlot(this, [this] {
569 sentryBreadcrumb("waveform", QStringLiteral("Set cells to 0"));
570 qCDebug(zero) << "Pressed 0.";
571 applyToSelectedCells([](int) { return 0; });
572 });
573}
574
575void BewavedDolphin::on_actionSetTo1_triggered()
576{
577 Application::guardedSlot(this, [this] {
578 sentryBreadcrumb("waveform", QStringLiteral("Set cells to 1"));
579 qCDebug(zero) << "Pressed 1.";
580 applyToSelectedCells([](int) { return 1; });
581 });
582}
583
584void BewavedDolphin::on_actionInvert_triggered()
585{
586 Application::guardedSlot(this, [this] {
587 sentryBreadcrumb("waveform", QStringLiteral("Invert cells"));
588 qCDebug(zero) << "Pressed Not.";
589 applyToSelectedCells([](int v) { return (v + 1) % 2; });
590 });
591}
592
593void BewavedDolphin::on_actionSetClockWave_triggered()
594{
595 Application::guardedSlot(this, [this] {
596 sentryBreadcrumb("waveform", QStringLiteral("Set clock wave"));
597 qCDebug(zero) << "Getting first column.";
598 const auto ranges = m_signalTableView->selectionModel()->selection();
599
600 if (ranges.isEmpty()) {
601 throw PANDACEPTION("No cells selected.");
602 }
603
604 // Anchor the clock phase to the leftmost selected column so the waveform
605 // starts at 0 regardless of where in the timeline the selection begins
606 const int firstCol = DolphinClipboard::firstColumn(*m_model, ranges);
607
608 qCDebug(zero) << "Setting the signal according to its column and clock period.";
609 ClockDialog dialog(m_clockPeriod, this);
610
611 if (dialog.exec() != QDialog::Accepted) {
612 return;
613 }
614
615 const int clockPeriod = dialog.period();
616 m_clockPeriod = clockPeriod;
617
618 const auto indexes = m_signalTableView->selectionModel()->selectedIndexes();
619 const auto [cells, oldValues] = snapshotCells(indexes);
620
621 DolphinEdits::clockWave(*m_model, indexes, firstCol, clockPeriod);
622
623 qCDebug(zero) << "Running simulation.";
624 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
625 m_edited = true;
626 run();
627 }));
628 });
629}
630
631void BewavedDolphin::on_actionCombinational_triggered()
632{
633 Application::guardedSlot(this, [this] {
634 sentryBreadcrumb("waveform", QStringLiteral("Combinational mode"));
635 const int truthTableSize = static_cast<int>((std::min)(static_cast<double>(SignalModel::kMaxColumns), std::pow(2, m_inputPorts)));
636 setLength(truthTableSize, false);
637
638 qCDebug(zero) << "Setting the signal according to its columns and clock period.";
639 const auto [cells, oldValues] = snapshotCells(allCellIndexes(m_inputPorts, m_model->columnCount()));
640
641 DolphinEdits::combinational(*m_model, m_inputPorts, m_model->columnCount());
642
643 qCDebug(zero) << "Running simulation.";
644 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
645 m_edited = true;
646 run();
647 }));
648 });
649}
650
651void BewavedDolphin::on_actionSetLength_triggered()
652{
653 Application::guardedSlot(this, [this] {
654 sentryBreadcrumb("waveform", QStringLiteral("Set length dialog"));
655 qCDebug(zero) << "Setting the simulation length.";
656 const int currentLength = m_length > 0 ? m_length : m_model->columnCount();
657 LengthDialog dialog(currentLength, this);
658
659 if (dialog.exec() != QDialog::Accepted) {
660 return;
661 }
662
663 setLength(dialog.length(), true);
664 });
665}
666
667void BewavedDolphin::setLength(const int simLength, const bool runSimulation)
668{
669 if (m_length == simLength) {
670 return;
671 }
672
673 m_length = simLength;
674
675 if (simLength <= m_model->columnCount()) {
676 // Shrinking: Qt's setColumnCount removes trailing columns automatically.
677 // New/removed columns inherit the Fixed default section size, so no resize needed.
678 qCDebug(zero) << "Reducing or keeping the simulation length.";
679 m_model->setColumnCount(simLength);
680 m_edited = true;
681 return;
682 }
683
684 // Growing: new input columns must be explicitly filled with zeros; output columns
685 // are populated by run() and don't need pre-filling
686 qCDebug(zero) << "Increasing the simulation length.";
687 const int oldLength = m_model->columnCount();
688 m_model->setColumnCount(simLength);
689 DolphinEdits::growInputColumns(*m_model, m_inputPorts, oldLength, simLength);
690
691 m_edited = true;
692 qCDebug(zero) << "Running simulation.";
693
694 if (runSimulation) {
695 run();
696 }
697}
698
699void BewavedDolphin::on_actionZoomOut_triggered()
700{
701 Application::guardedSlot(this, [this] {
702 sentryBreadcrumb("waveform", QStringLiteral("Zoom out"));
703 m_zoom->zoomOut();
704 zoomChanged();
705 });
706}
707
708void BewavedDolphin::on_actionZoomIn_triggered()
709{
710 Application::guardedSlot(this, [this] {
711 sentryBreadcrumb("waveform", QStringLiteral("Zoom in"));
712 m_zoom->zoomIn();
713 zoomChanged();
714 });
715}
716
717void BewavedDolphin::on_actionResetZoom_triggered()
718{
719 Application::guardedSlot(this, [this] {
720 sentryBreadcrumb("waveform", QStringLiteral("Zoom reset"));
721 m_zoom->reset();
722 zoomChanged();
723 });
724}
725
726void BewavedDolphin::zoomChanged()
727{
728 m_ui->actionZoomIn->setEnabled(m_zoom->canZoomIn());
729 m_ui->actionZoomOut->setEnabled(m_zoom->canZoomOut());
730}
731
732void BewavedDolphin::on_actionFitScreen_triggered()
733{
734 Application::guardedSlot(this, [this] {
735 sentryBreadcrumb("waveform", QStringLiteral("Fit screen"));
736 m_zoom->fitScreen();
737 zoomChanged();
738 });
739}
740
741void BewavedDolphin::on_actionClear_triggered()
742{
743 Application::guardedSlot(this, [this] {
744 sentryBreadcrumb("waveform", QStringLiteral("Clear input"));
745 const auto [cells, oldValues] = snapshotCells(allCellIndexes(m_inputPorts, m_model->columnCount()));
746
747 DolphinEdits::clearInputs(*m_model, m_inputPorts);
748
749 qCDebug(zero) << "Running simulation.";
750 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
751 m_edited = true;
752 run();
753 }));
754 });
755}
756
757void BewavedDolphin::on_actionAutoCrop_triggered()
758{
759 Application::guardedSlot(this, [this] {
760 sentryBreadcrumb("waveform", QStringLiteral("Auto crop"));
761 const int lastNonZero = DolphinEdits::lastNonZeroColumn(*m_model, m_inputPorts);
762 setLength(lastNonZero + 1, true);
763 });
764}
765
766void BewavedDolphin::on_actionCopy_triggered()
767{
768 Application::guardedSlot(this, [this] {
769 sentryBreadcrumb("clipboard", QStringLiteral("Waveform copy"));
770 const auto ranges = m_signalTableView->selectionModel()->selection();
771
772 if (ranges.isEmpty()) {
773 QApplication::clipboard()->clear();
774 return;
775 }
776
777 DolphinClipboard::copyToClipboard(*m_model, ranges);
778 });
779}
780
781void BewavedDolphin::on_actionCut_triggered()
782{
783 Application::guardedSlot(this, [this] {
784 sentryBreadcrumb("clipboard", QStringLiteral("Waveform cut"));
785 const auto ranges = m_signalTableView->selectionModel()->selection();
786
787 if (ranges.isEmpty()) {
788 QApplication::clipboard()->clear();
789 return;
790 }
791
792 // Cut = copy the selection, then clear it (which re-runs the simulation).
793 DolphinClipboard::copyToClipboard(*m_model, ranges);
794 on_actionSetTo0_triggered();
795 m_edited = true;
796 });
797}
798
799void BewavedDolphin::on_actionPaste_triggered()
800{
801 Application::guardedSlot(this, [this] {
802 sentryBreadcrumb("clipboard", QStringLiteral("Waveform paste"));
803 const auto ranges = m_signalTableView->selectionModel()->selection();
804
805 if (ranges.isEmpty()) {
806 return;
807 }
808
809 if (DolphinClipboard::pasteFromClipboard(*m_model, ranges)) {
810 m_edited = true;
811 run();
812 }
813 });
814}
815
816void BewavedDolphin::on_actionSave_triggered()
817{
818 Application::guardedSlot(this, [this] {
819 sentryBreadcrumb("file", QStringLiteral("Waveform save"));
820 if (m_currentFile.fileName().isEmpty()) {
821 on_actionSaveAs_triggered();
822 return;
823 }
824
825 DolphinFile::save(*m_model, m_currentFile.absoluteFilePath(), m_inputPorts);
826 m_ui->statusbar->showMessage(tr("Saved file successfully."), 4000);
827 m_edited = false;
828 });
829}
830
831void BewavedDolphin::on_actionSaveAs_triggered()
832{
833 Application::guardedSlot(this, [this] {
834 sentryBreadcrumb("file", QStringLiteral("Waveform save as"));
835
836 // List the format that matches the current file first so it is the default selection
837 const QString fileFilter = m_currentFile.fileName().endsWith(".csv") ?
838 tr("CSV files") + " (*.csv);;" + tr("Dolphin files") + " (*.dolphin);;" + tr("All supported files") + " (*.dolphin *.csv)"
839 : tr("Dolphin files") + " (*.dolphin);;" + tr("CSV files") + " (*.csv);;" + tr("All supported files") + " (*.dolphin *.csv)";
840
841 const QString initialPath = m_currentFile.fileName().isEmpty()
842 ? m_host->currentFile().absolutePath()
843 : m_currentFile.absoluteFilePath();
844
845 const auto result = FileDialogs::provider()->getSaveFileName(this, tr("Save File as..."), initialPath, fileFilter);
846 QString fileName = result.fileName;
847
848 if (fileName.isEmpty()) {
849 return;
850 }
851
852 // Append the correct extension when the user types a bare name without one,
853 // inferring the format from whichever filter was active in the dialog
854 if (!fileName.endsWith(".dolphin") && !fileName.endsWith(".csv")) {
855 if (result.selectedFilter.contains("dolphin")) {
856 fileName.append(".dolphin");
857 } else {
858 fileName.append(".csv");
859 }
860 }
861
862 DolphinFile::save(*m_model, fileName, m_inputPorts);
863 m_currentFile = QFileInfo(fileName);
864 associateToWiRedPanda(fileName);
865 setWindowTitle(tr("beWavedDolphin Simulator") + " [" + m_currentFile.fileName() + "]");
866 m_ui->statusbar->showMessage(tr("Saved file successfully."), 4000);
867 m_edited = false;
868 });
869}
870
871void BewavedDolphin::associateToWiRedPanda(const QString &fileName)
872{
873 // Only prompt when the file is new (not already linked) and we are in interactive mode;
874 // non-interactive (command-line / test) sessions skip the dialog entirely
875 if ((m_host->dolphinFileName() != fileName) && Application::interactiveMode) {
876 const auto reply =
877 QMessageBox::question(
878 this,
879 tr("wiRedPanda - beWavedDolphin"),
880 tr("Do you want to link this beWavedDolphin file to your current wiRedPanda file and save it?"),
881 QMessageBox::Yes | QMessageBox::No);
882
883 if (reply == QMessageBox::Yes) {
884 m_host->setDolphinFileName(fileName);
885 m_host->save({});
886 }
887 }
888}
889
890void BewavedDolphin::on_actionLoad_triggered()
891{
892 Application::guardedSlot(this, [this] {
893 sentryBreadcrumb("file", QStringLiteral("Waveform load dialog"));
894 QDir defaultDirectory;
895
896 // Prefer the last-used dolphin file's directory; fall back to the main window's
897 // working directory, and finally to the home directory
898 if (m_currentFile.exists()) {
899 defaultDirectory.setPath(m_currentFile.absolutePath());
900 } else {
901 if (m_host->currentFile().exists()) {
902 m_host->currentFile().dir();
903 } else {
904 defaultDirectory.setPath(QDir::homePath());
905 }
906 }
907
908 const QString homeDir(m_host->currentDir().absolutePath());
909
910 const QString fileName = FileDialogs::provider()->getOpenFileName(
911 this, tr("Open File"), homeDir,
912 tr("All supported files") + " (*.dolphin *.csv);;" + tr("Dolphin files") + " (*.dolphin);;" + tr("CSV files") + " (*.csv)");
913
914 if (fileName.isEmpty()) {
915 return;
916 }
917
918 load(fileName);
919 m_edited = false;
920 m_ui->statusbar->showMessage(tr("File loaded successfully."), 4000);
921 });
922}
923
924void BewavedDolphin::load(const QString &fileName)
925{
926 // DolphinFile handles the on-disk format; we apply the parsed input rows and
927 // record the association with the circuit file.
928 applyWaveformData(DolphinFile::load(fileName, m_inputPorts));
929 m_currentFile = QFileInfo(fileName);
930 associateToWiRedPanda(fileName);
931 setWindowTitle(tr("beWavedDolphin Simulator") + " [" + m_currentFile.fileName() + "]");
932}
933
934void BewavedDolphin::applyWaveformData(const DolphinSerializer::WaveformData &fileData)
935{
936 setLength(fileData.columns, false);
937 qCDebug(zero) << "Update table.";
938
939 {
940 SignalModel::BulkEditGuard guard(*m_model);
941 for (int row = 0; row < fileData.inputPorts; ++row) {
942 for (int col = 0; col < fileData.columns; ++col) {
943 m_model->setValue(row, col, fileData.values[row * fileData.columns + col]);
944 }
945 }
946 }
947
948 run();
949}
950
951void BewavedDolphin::on_actionShowNumbers_triggered()
952{
953 Application::guardedSlot(this, [this] {
954 sentryBreadcrumb("waveform", QStringLiteral("Show numbers"));
955 // Display mode is a pure view concern now: the model keeps the same values and the
956 // delegate switches between numeric text and waveform rendering.
957 m_delegate->setPlotType(PlotType::Number);
958 m_signalTableView->viewport()->update();
959 });
960}
961
962void BewavedDolphin::on_actionShowWaveforms_triggered()
963{
964 Application::guardedSlot(this, [this] {
965 sentryBreadcrumb("waveform", QStringLiteral("Show waveforms"));
966 m_delegate->setPlotType(PlotType::Line);
967 m_signalTableView->viewport()->update();
968 });
969}
970
971void BewavedDolphin::on_actionExportToPng_triggered()
972{
973 Application::guardedSlot(this, [this] {
974 sentryBreadcrumb("export", QStringLiteral("Waveform export PNG"));
975 QString pngFile = FileDialogs::provider()->getSaveFileName(this, tr("Export to Image"), m_currentFile.absolutePath(), tr("PNG files") + " (*.png)").fileName;
976
977 if (pngFile.isEmpty()) {
978 return;
979 }
980
981 if (!pngFile.endsWith(".png", Qt::CaseInsensitive)) {
982 pngFile.append(".png");
983 }
984
985 DolphinExporter::exportToPng(m_model, m_delegate->plotType(), pngFile);
986 });
987}
988
989void BewavedDolphin::on_actionExportToPdf_triggered()
990{
991 Application::guardedSlot(this, [this] {
992 sentryBreadcrumb("export", QStringLiteral("Waveform export PDF"));
993 QString pdfFile = FileDialogs::provider()->getSaveFileName(this, tr("Export to PDF"), m_currentFile.absolutePath(), tr("PDF files") + " (*.pdf)").fileName;
994
995 if (pdfFile.isEmpty()) {
996 return;
997 }
998
999 if (!pdfFile.endsWith(".pdf", Qt::CaseInsensitive)) {
1000 pdfFile.append(".pdf");
1001 }
1002
1003 DolphinExporter::exportToPdf(m_model, m_delegate->plotType(), pdfFile);
1004 });
1005}
1006
1007void BewavedDolphin::on_actionAbout_triggered()
1008{
1009 Application::guardedSlot(this, [this] {
1010 QMessageBox::about(this,
1011 "beWavedDolphin",
1012 tr("<p>beWavedDolphin is a waveform simulator for wiRedPanda, developed by the Federal University of São Paulo"
1013 " to help students learn about logic circuits.</p>"
1014 "<p>Software version: %1</p>"
1015 "<p><strong>Creators:</strong></p>"
1016 "<ul>"
1017 "<li> Prof. Fábio Cappabianco, Ph.D. </li>"
1018 "</ul>"
1019 "<p> beWavedDolphin is currently maintained by Prof. Fábio Cappabianco, Ph.D. and his students.</p>"
1020 "<p> Please file a report at our GitHub page if you find a bug or want to request a new feature.</p>"
1021 "<p><a href=\"http://gibis-unifesp.github.io/wiRedPanda/\">Visit our website!</a></p>")
1022 .arg(QApplication::applicationVersion()));
1023 });
1024}
1025
1026void BewavedDolphin::on_actionAboutQt_triggered()
1027{
1028 Application::guardedSlot(this, [this] {
1029 QMessageBox::aboutQt(this);
1030 });
1031}
Custom QApplication subclass with exception handling and main-window access.
BewavedDolphin waveform editor: digital signal creation, display, and export.
ClockDialog: dialog for selecting a clock wave's period (in time-step columns).
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
#define qCDebug(category)
Definition Common.h:29
DolphinClipboard: copy/paste of waveform cell rectangles to/from a data stream.
DolphinCommands: undo/redo commands for the beWavedDolphin waveform editor.
DolphinEdits: pure value-grid mutations for the beWavedDolphin waveform table.
DolphinExporter: renders/serializes a SignalModel to images and text artifacts.
DolphinFile: reads/writes a SignalModel to .dolphin/.csv files on disk.
DolphinHost: the host-application context the beWavedDolphin editor depends on.
DolphinModelBuilder: maps a circuit scene's I/O elements to waveform signal rows.
DolphinZoom: zoom state and metrics for the beWavedDolphin waveform table.
Abstract file dialog interface for testability.
Abstract base class for user-controllable input elements.
Abstract base class for all graphical circuit elements.
Extended QGraphicsView with zoom, pan, and fast-rendering modes.
LengthDialog: dialog for setting the BeWavedDolphin simulation length.
Lightweight Sentry helpers gated behind HAVE_SENTRY.
void sentryBreadcrumb(const char *category, const QString &message)
DolphinSerializer: encoding and decoding of .dolphin and .csv waveform files.
Typed wrappers around QSettings for all application preferences.
@ Line
Cells display a waveform-style rising/falling edge graphic.
@ Number
Cells display the numeric value (0/1).
Drives the wiRedPanda circuit simulation column-by-column for the waveform table.
static void guardedSlot(const QObject *context, Body &&body) noexcept
Wraps a slot body in try/catch and reports any exception synchronously, inside the slot's own stack f...
static bool interactiveMode
Definition Application.h:73
Hand-written UI descriptor for the BeWavedDolphin main window.
void print()
Prints the waveform table to the system printer.
int inputRow(const QString &label) const
Returns the input row index whose element label equals label, or -1 (MCP access).
void run()
Runs the simulation for all input combinations and fills output rows.
void createWaveform()
Initializes a blank waveform from the current scene's I/O elements.
~BewavedDolphin() override
void setExerciseOverlay(ExerciseOverlay *overlay)
void setCellValue(const int row, const int col, const int value)
Sets a single cell value in the waveform table.
QAction * actionCombinational() const
Returns the combinational action (for tour button spotlighting).
void prepare(const QString &fileName={})
Prepares the waveform from fileName (or blank if empty).
void saveToTxt(QTextStream &stream)
Exports the waveform data as plain text to stream.
WaveformSnapshot snapshot(int duration) const
Returns the input/output signals over the first duration columns (MCP access).
bool exportToPng(const QString &filename)
Exports the waveform scene to a PNG image file.
QToolBar * mainToolBar() const
Returns the main toolbar (for tour target resolution).
void show()
Shows the window and loads the initial waveform.
void resizeEvent(QResizeEvent *event) override
bool eventFilter(QObject *watched, QEvent *event) override
void triggerCombinational()
Triggers the combinational input-pattern generator (for tour automation).
void closeEvent(QCloseEvent *event) override
BewavedDolphin(Scene *scene, const bool askConnection=true, DolphinHost *host=nullptr, QWidget *parent=nullptr)
Constructs the waveform editor.
void setLength(const int simLength, const bool runSimulation=false)
Sets the number of time-step columns.
Interface the host application (MainWindow) provides to BewavedDolphin.
Definition DolphinHost.h:24
Semi-transparent overlay displayed at the bottom of the canvas during a circuit exercise.
virtual FileDialogResult getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
virtual QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
Main circuit editing scene.
Definition Scene.h:56
static QByteArray dolphinGeometry()
Definition Settings.cpp:85
static bool labelsUnderIcons()
Definition Settings.cpp:107
static void setDolphinGeometry(const QByteArray &geometry)
Definition Settings.cpp:90
Item delegate that draws digital waveform graphics inside table cells.
QStandardItemModel subclass that makes all cells non-editable.
Definition SignalModel.h:21
void setValue(int row, int col, int value)
Sets the logic value (0/1) of the cell at (row, col).
int value(int row, int col) const
Returns the logic value (0/1) of the cell at (row, col).
void setInputRows(int inputRows)
Records how many leading rows are inputs (the rest are outputs).
static constexpr int kMaxColumns
Definition SignalModel.h:38
static QVector< Status > captureInputs(const QVector< GraphicElementInput * > &inputs, int inputPorts)
Snapshots the live output-port state of every input element.
static void restoreInputs(const QVector< GraphicElementInput * > &inputs, const QVector< Status > &saved)
Restores input-port states previously captured by captureInputs().
bool pasteFromClipboard(SignalModel &model, const QItemSelection &ranges)
void copyToClipboard(const SignalModel &model, const QItemSelection &ranges)
int firstColumn(const SignalModel &model, const QItemSelection &ranges)
Returns the leftmost column index in ranges (clamped to the model's columns).
void combinational(SignalModel &model, const int inputPorts, const int columns)
void clearInputs(SignalModel &model, const int inputPorts)
Sets every input cell (the first inputPorts rows, over the model's columns) to 0.
void applyToCells(SignalModel &model, const QModelIndexList &cells, const std::function< int(int)> &valueFn)
Sets each cell in cells to valueFn(currentValue).
void growInputColumns(SignalModel &model, const int inputPorts, const int oldLength, const int newLength)
int lastNonZeroColumn(const SignalModel &model, const int inputPorts)
Returns the index of the last column with any non-zero input value, or 0 if none.
void clockWave(SignalModel &model, const QModelIndexList &cells, const int firstCol, const int period)
void exportToPdf(const SignalModel *model, const PlotType plotType, const QString &fileName)
void writeTruthTableText(QTextStream &out, const SignalModel *model, const int inputRowCount)
QString csvText(const SignalModel *model)
bool exportToPng(const SignalModel *model, const PlotType plotType, const QString &fileName)
DolphinSerializer::WaveformData parseTerminal(QTextStream &in, const int maxInputPorts)
DolphinSerializer::WaveformData load(const QString &fileName, const int maxInputPorts)
void save(const SignalModel &model, const QString &fileName, const int inputPorts)
Writes model to fileName atomically, choosing the format by extension.
Signals collect(Scene *scene)
FileDialogProvider * provider()
Returns the active provider. Never null.
A labelled waveform signal: a row label and its per-column 0/1 values.
QVector< int > values
Per-column logic values.
QString label
The signal's display label.
A read-only snapshot of the computed waveform, for automation/export consumers.
QList< Signal > outputs
Output signals (one per output element).
QList< Signal > inputs
Input signals (one per input element).
Raw waveform data returned by the load functions.
Definition Serializer.h:33
int inputPorts
Number of input rows stored in values.
Definition Serializer.h:34
QVector< int > values
Cell values in row-major order (inputs only): index = row * columns + col.
Definition Serializer.h:37
QString fileName
Selected file path, empty if cancelled.