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 (m_host && (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 m_rows = tableSignals.rows;
183
184 qCDebug(zero) << "Loading initial data into the table.";
185 loadNewTable(tableSignals.inputLabels, tableSignals.outputLabels);
186}
187
188void BewavedDolphin::loadNewTable(const QStringList &inputLabels, const QStringList &outputLabels)
189{
190 qCDebug(zero) << "Snapshotting current input values into oldvalues, to restore after simulation.";
191 // Snapshot the live input port states before the simulation sweep overwrites them, so
192 // they can be restored after the sweep completes.
193 m_oldInputValues = WaveformSimulator::captureInputs(m_inputs, m_inputPorts);
194
195 qCDebug(zero) << "Num iter = " << m_length;
196
197 // Rows = total signals (inputs + outputs); columns = simulation length in time steps.
198 // loadNewTable() runs exactly once per window (each is WA_DeleteOnClose), so this is a
199 // single allocation; parenting to `this` ties its lifetime to the window — not re-created.
200 m_model = new SignalModel(static_cast<int>(inputLabels.size() + outputLabels.size()), m_length, this);
201 m_signalTableView->setModel(m_model);
202
203 // Input rows come first, then output rows — the split point is inputLabels.size()
204 m_model->setVerticalHeaderLabels(inputLabels + outputLabels);
205 m_model->setInputRows(static_cast<int>(inputLabels.size()));
206
207 // The delegate derives each cell's waveform from the model; it owns the current mode.
208
209 // A cell's rising/falling edge depends on its left neighbour, so any value change must
210 // repaint the whole visible grid (update() coalesces, so a full sweep is one repaint).
211 connect(m_model, &QAbstractItemModel::dataChanged, this, [this] {
212 m_signalTableView->viewport()->update();
213 });
214
215 m_signalTableView->setAlternatingRowColors(true);
216 m_signalTableView->setShowGrid(false);
217
218 // Fixed section sizes keep waveforms aligned; applyZoom() drives the actual
219 // row/column sizes from the current zoom factor.
220 m_signalTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Fixed);
221 m_signalTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Fixed);
222
223 qCDebug(zero) << "Inputs: " << inputLabels.size() << ", outputs: " << outputLabels.size();
224
225 // Initialise all input cells to 0 and compute the first output sweep
226 on_actionClear_triggered();
227
228 connect(m_signalTableView, &QAbstractItemView::doubleClicked, this, &BewavedDolphin::on_tableView_cellDoubleClicked);
229 connect(m_signalTableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &BewavedDolphin::on_tableView_selectionChanged);
230
231 // Size rows/columns/font for the current zoom so the table is correct even
232 // before the window is shown (e.g. in headless tests and offscreen export).
233 applyZoom();
234}
235
236void BewavedDolphin::on_tableView_cellDoubleClicked()
237{
238 // Toggle each selected cell between 0 and 1 -- identical to Invert, so route through the
239 // same undoable helper rather than duplicating the DolphinEdits call inline.
240 applyToSelectedCells([](int v) { return (v + 1) % 2; });
241}
242
243void BewavedDolphin::on_tableView_selectionChanged()
244{
245 m_externalScene->clearSelection();
246
247 const auto indexes = m_signalTableView->selectionModel()->selectedIndexes();
248
249 m_ui->actionSetClockWave->setEnabled(!indexes.isEmpty());
250
251 // Highlight the corresponding input element in the circuit editor when the user
252 // selects a waveform row, giving visual feedback about which signal they are editing.
253 // Output rows (index >= m_inputs.size()) have no element to highlight.
254 for (auto &index : indexes) {
255 if (index.row() < m_inputs.size()) {
256 m_inputs.at(index.row())->setSelected(true);
257 }
258 }
259
260 m_externalScene->view()->update();
261}
262
263bool BewavedDolphin::elementsStillLive() const
264{
265 const auto sceneElements = m_externalScene->elements();
266 for (auto *elm : std::as_const(m_inputs)) {
267 if (!sceneElements.contains(elm)) {
268 return false;
269 }
270 }
271 for (auto *elm : std::as_const(m_outputs)) {
272 if (!sceneElements.contains(elm)) {
273 return false;
274 }
275 }
276 return true;
277}
278
280{
281 // Guard against a caller invoking run() before prepare()/loadNewTable() has built
282 // the sweep driver and table model (every current call site prepares first, but
283 // this is a public entry point with no compile-time guarantee of that ordering).
284 if (!m_simDriver || !m_model) {
285 return;
286 }
287
288 // An input/output element this waveform was built from may have been deleted from the
289 // live scene since prepare() ran — via the main canvas, or an MCP client, neither of
290 // which this window's modality blocks (MCP mutates the scene directly, bypassing Qt
291 // input routing entirely). Skip the sweep rather than dereferencing freed elements.
292 if (!elementsStillLive()) {
293 return;
294 }
295
296 // Drive the circuit across every time column. Inputs are read from the model and the
297 // computed outputs (isInput=false → green; changeNext=false → caller refreshes) are
298 // written back, then the original input states are restored so the live simulation
299 // resumes correctly.
300 {
301 SignalModel::BulkEditGuard guard(*m_model);
302 m_simDriver->sweep(
303 m_rows, m_model->columnCount(),
304 [this](int row, int col) { return m_model->value(row, col) != 0; },
305 [this](int row, int col, int value) { m_model->setValue(row, col, value); });
306 }
307
308 qCDebug(three) << "Setting inputs back to old values.";
309 WaveformSimulator::restoreInputs(m_inputs, m_oldInputValues);
310}
311
312bool BewavedDolphin::eventFilter(QObject *watched, QEvent *event)
313{
314 // The mouse wheel zooms the columns (the original beWavedDolphin behavior): any
315 // wheel over the table viewport widens/narrows the columns and is consumed, so the
316 // table never scrolls on wheel. Use the scrollbar to pan a long waveform.
317 if (watched == m_signalTableView->viewport() && event->type() == QEvent::Wheel) {
318 auto *wheel = static_cast<QWheelEvent *>(event);
319 if (wheel->angleDelta().y() > 0) {
320 on_actionZoomIn_triggered();
321 } else if (wheel->angleDelta().y() < 0) {
322 on_actionZoomOut_triggered();
323 }
324 return true;
325 }
326
327 return QMainWindow::eventFilter(watched, event);
328}
329
330void BewavedDolphin::applyZoom()
331{
332 // The zoom math lives in DolphinZoom; the controller drives the action enable-state.
333 m_zoom->apply();
334 zoomChanged();
335}
336
337void BewavedDolphin::on_actionExit_triggered()
338{
339 Application::guardedSlot(this, [this] {
340 close();
341 });
342}
343
344void BewavedDolphin::closeEvent(QCloseEvent *event)
345{
346 // askConnection gates whether closing consults checkSave() at all — with it false, the
347 // window must still be closable, just without the save-changes prompt.
348 if (m_askConnection && !checkSave()) {
349 event->ignore();
350 return;
351 }
352
353 event->accept();
354}
355
356void BewavedDolphin::resizeEvent(QResizeEvent *event)
357{
358 QMainWindow::resizeEvent(event);
359 if (m_exerciseOverlay && m_exerciseOverlay->isVisible()) {
360 m_exerciseOverlay->repositionToParent();
361 }
362}
363
365{
366 m_exerciseOverlay = overlay;
367}
368
370{
371 return m_ui->mainToolBar;
372}
373
375{
376 return m_ui->actionCombinational;
377}
378
380{
381 on_actionCombinational_triggered();
382}
383
384bool BewavedDolphin::checkSave()
385{
386 if (!m_edited) {
387 return true;
388 }
389
390 auto reply =
391 QMessageBox::question(
392 this,
393 tr("wiRedPanda - beWavedDolphin"),
394 tr("Save simulation before closing?"),
395 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
396
397 switch (reply) {
398 // After saving, check m_edited again: if the save itself failed (e.g. user dismissed
399 // the Save As dialog), m_edited stays true and we should not allow the close
400 case QMessageBox::Save: on_actionSave_triggered(); return (!m_edited);
401 case QMessageBox::Discard: return true;
402 case QMessageBox::Cancel: return false;
403 // Not reachable: the dialog above offers exactly these 3 StandardButtons, so
404 // QMessageBox::question() can only ever return one of them.
405 default: return true; // LCOV_EXCL_LINE
406 }
407}
408
409void BewavedDolphin::setCellValue(const int row, const int col, const int value)
410{
411 // The model stores only the logic value; the delegate derives the waveform segment
412 // (from this cell and its left neighbour) and the input/output colour at paint time.
413 m_model->setValue(row, col, value);
414}
415
416int BewavedDolphin::inputRow(const QString &label) const
417{
418 // Indexes m_rows, not m_inputs: a multi-port input occupies one row PER PORT, so an
419 // element index is not a row index. Matches the per-port row label ("Bus[0]"), which is
420 // also what the table header and snapshot() report, so all three agree on one name.
421 for (int row = 0; row < m_rows.size(); ++row) {
422 const auto &descriptor = m_rows.at(row);
423 if (descriptor.kind == DolphinModelBuilder::RowKind::Input && descriptor.label == label) {
424 return row;
425 }
426 }
427
428 return -1;
429}
430
432{
433 // Read the computed waveform into a plain DTO so automation consumers (the MCP server)
434 // need neither the live model nor the element vectors.
435 //
436 // Walks m_rows, which IS the table's row layout, rather than re-deriving it from the
437 // element vectors: a row is one PORT, so an element index is not a row index, and the
438 // first output row is at the input PORT count rather than at m_inputs.size().
439 WaveformSnapshot result;
440
441 for (int row = 0; row < m_rows.size(); ++row) {
442 const auto &descriptor = m_rows.at(row);
443
444 Signal signal;
445 signal.label = descriptor.label;
446 for (int col = 0; col < duration; ++col) {
447 signal.values.append(m_model->value(row, col));
448 }
449
450 if (descriptor.kind == DolphinModelBuilder::RowKind::Input) {
451 result.inputs.append(signal);
452 } else {
453 result.outputs.append(signal);
454 }
455 }
456
457 return result;
458} // LCOV_EXCL_LINE -- compiler-generated WaveformSnapshot cleanup for an exception path snapshot() never takes
459
461{
462 QMainWindow::show();
463 applyZoom();
464}
465
467{
468 if (!m_model) {
469 return;
470 }
471 // Outputs in the same CSV format used by loadFromTerminal() / the CSV save path,
472 // allowing round-trip scripted use without a GUI.
473 QTextStream(stdout) << DolphinExporter::csvText(m_model);
474}
475
476void BewavedDolphin::saveToTxt(QTextStream &stream)
477{
478 if (!m_model) {
479 return;
480 }
481
482 // Same staleness hazard as run() (see elementsStillLive()); saveToTxt() dereferences
483 // m_inputs/m_outputs via sweep() below too. Throw instead of silently no-op'ing so an
484 // MCP client (the only realistic caller once run() already guards interactive use) gets
485 // a clean error instead of a "successful" empty/truncated export.
486 if (!elementsStillLive()) {
487 throw PANDACEPTION("Cannot export: the circuit this waveform was built from has changed.");
488 }
489
490 // Dump the full combinational truth table. Build it in a throwaway model (like
491 // renderWaveform() uses a throwaway view) so the live document is never mutated —
492 // exporting must not clobber the user's waveform (e.g. an MCP persistent session).
493 // Clamp to SignalModel::kMaxColumns, exactly like on_actionCombinational_triggered's
494 // identical cap below — otherwise a circuit with ~25+ effective input ports allocates
495 // tens of millions of cells, and m_inputPorts >= 31 overflows the int cast (UB).
496 const int columns = static_cast<int>((std::min)(static_cast<double>(SignalModel::kMaxColumns), std::pow(2, m_inputPorts)));
497 SignalModel truthTable(m_model->rowCount(), columns);
498
499 // Carry the live model's row labels onto the throwaway model so the exporter, which
500 // reads labels and values from a single model, formats the same headers as before.
501 QStringList labels;
502 labels.reserve(m_model->rowCount());
503 for (int row = 0; row < m_model->rowCount(); ++row) {
504 labels.append(m_model->verticalHeaderItem(row)->text());
505 }
506 truthTable.setVerticalHeaderLabels(labels);
507
508 DolphinEdits::combinational(truthTable, m_inputPorts, columns);
509
510 // Compute the outputs for every input combination, then restore the live inputs the
511 // sweep perturbed (the same capture/restore contract run() relies on).
512 {
513 SignalModel::BulkEditGuard guard(truthTable);
514 m_simDriver->sweep(
515 m_rows, columns,
516 [&truthTable](int row, int col) { return truthTable.value(row, col) != 0; },
517 [&truthTable](int row, int col, int value) { truthTable.setValue(row, col, value); });
518 }
519 WaveformSimulator::restoreInputs(m_inputs, m_oldInputValues);
520
521 DolphinExporter::writeTruthTableText(stream, &truthTable, static_cast<int>(m_inputs.size()));
522}
523
524bool BewavedDolphin::exportToPng(const QString &filename)
525{
526 // Public façade for the MCP server: swallow exceptions and report success as a bool.
527 // Not reachable as currently implemented: DolphinExporter::renderToPixmap() only calls
528 // plain Qt widget/painting APIs (none of which throw) and QPixmap::save() reports
529 // failure via its bool return, not an exception — but the catch stays as a defensive
530 // backstop against a future renderToPixmap()/save() change that does throw.
531 try {
532 return DolphinExporter::exportToPng(m_model, m_delegate->plotType(), filename);
533 } catch (...) { // LCOV_EXCL_LINE
534 return false; // LCOV_EXCL_LINE
535 } // LCOV_EXCL_LINE
536} // LCOV_EXCL_LINE
537
538std::pair<QList<QPair<int, int>>, QList<int>> BewavedDolphin::snapshotCells(const QModelIndexList &indexes) const
539{
540 QList<QPair<int, int>> cells;
541 QList<int> oldValues;
542 cells.reserve(indexes.size());
543 oldValues.reserve(indexes.size());
544 for (const auto &idx : indexes) {
545 cells.append({idx.row(), idx.column()});
546 oldValues.append(m_model->value(idx.row(), idx.column()));
547 }
548 return {cells, oldValues};
549}
550
551QModelIndexList BewavedDolphin::allCellIndexes(int rows, int cols) const
552{
553 QModelIndexList indexes;
554 indexes.reserve(rows * cols);
555 for (int row = 0; row < rows; ++row) {
556 for (int col = 0; col < cols; ++col) {
557 indexes.append(m_model->index(row, col));
558 }
559 }
560 return indexes;
561} // LCOV_EXCL_LINE -- compiler-generated QModelIndexList cleanup for an exception path allCellIndexes() never takes
562
563void BewavedDolphin::applyToSelectedCells(const std::function<int(int)> &valueFn)
564{
565 const auto indexes = m_signalTableView->selectionModel()->selectedIndexes();
566 const auto [cells, oldValues] = snapshotCells(indexes);
567
568 DolphinEdits::applyToCells(*m_model, indexes, valueFn);
569
570 qCDebug(zero) << "Running simulation.";
571 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
572 m_edited = true;
573 run();
574 }));
575}
576
577void BewavedDolphin::on_actionSetTo0_triggered()
578{
579 Application::guardedSlot(this, [this] {
580 sentryBreadcrumb("waveform", QStringLiteral("Set cells to 0"));
581 qCDebug(zero) << "Pressed 0.";
582 applyToSelectedCells([](int) { return 0; });
583 });
584}
585
586void BewavedDolphin::on_actionSetTo1_triggered()
587{
588 Application::guardedSlot(this, [this] {
589 sentryBreadcrumb("waveform", QStringLiteral("Set cells to 1"));
590 qCDebug(zero) << "Pressed 1.";
591 applyToSelectedCells([](int) { return 1; });
592 });
593}
594
595void BewavedDolphin::on_actionInvert_triggered()
596{
597 Application::guardedSlot(this, [this] {
598 sentryBreadcrumb("waveform", QStringLiteral("Invert cells"));
599 qCDebug(zero) << "Pressed Not.";
600 applyToSelectedCells([](int v) { return (v + 1) % 2; });
601 });
602}
603
604void BewavedDolphin::on_actionSetClockWave_triggered()
605{
606 Application::guardedSlot(this, [this] {
607 sentryBreadcrumb("waveform", QStringLiteral("Set clock wave"));
608 qCDebug(zero) << "Getting first column.";
609 const auto ranges = m_signalTableView->selectionModel()->selection();
610
611 if (ranges.isEmpty()) {
612 throw PANDACEPTION("No cells selected.");
613 }
614
615 // Anchor the clock phase to the leftmost selected column so the waveform
616 // starts at 0 regardless of where in the timeline the selection begins
617 const int firstCol = DolphinClipboard::firstColumn(*m_model, ranges);
618
619 qCDebug(zero) << "Setting the signal according to its column and clock period.";
620 ClockDialog dialog(m_clockPeriod, this);
621
622 if (dialog.exec() != QDialog::Accepted) {
623 return;
624 }
625
626 const int clockPeriod = dialog.period();
627 m_clockPeriod = clockPeriod;
628
629 const auto indexes = m_signalTableView->selectionModel()->selectedIndexes();
630 const auto [cells, oldValues] = snapshotCells(indexes);
631
632 DolphinEdits::clockWave(*m_model, indexes, firstCol, clockPeriod);
633
634 qCDebug(zero) << "Running simulation.";
635 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
636 m_edited = true;
637 run();
638 }));
639 });
640}
641
642void BewavedDolphin::on_actionCombinational_triggered()
643{
644 Application::guardedSlot(this, [this] {
645 sentryBreadcrumb("waveform", QStringLiteral("Combinational mode"));
646 const int truthTableSize = static_cast<int>((std::min)(static_cast<double>(SignalModel::kMaxColumns), std::pow(2, m_inputPorts)));
647 setLength(truthTableSize, false);
648
649 qCDebug(zero) << "Setting the signal according to its columns and clock period.";
650 const auto [cells, oldValues] = snapshotCells(allCellIndexes(m_inputPorts, m_model->columnCount()));
651
652 DolphinEdits::combinational(*m_model, m_inputPorts, m_model->columnCount());
653
654 qCDebug(zero) << "Running simulation.";
655 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
656 m_edited = true;
657 run();
658 }));
659 });
660}
661
662void BewavedDolphin::on_actionSetLength_triggered()
663{
664 Application::guardedSlot(this, [this] {
665 sentryBreadcrumb("waveform", QStringLiteral("Set length dialog"));
666 qCDebug(zero) << "Setting the simulation length.";
667 const int currentLength = m_length > 0 ? m_length : m_model->columnCount();
668 LengthDialog dialog(currentLength, this);
669
670 if (dialog.exec() != QDialog::Accepted) {
671 return;
672 }
673
674 setLength(dialog.length(), true);
675 });
676}
677
678void BewavedDolphin::setLength(const int simLength, const bool runSimulation)
679{
680 if (m_length == simLength) {
681 return;
682 }
683
684 m_length = simLength;
685
686 if (simLength <= m_model->columnCount()) {
687 // Shrinking: Qt's setColumnCount removes trailing columns automatically.
688 // New/removed columns inherit the Fixed default section size, so no resize needed.
689 qCDebug(zero) << "Reducing or keeping the simulation length.";
690 m_model->setColumnCount(simLength);
691 m_edited = true;
692 return;
693 }
694
695 // Growing: new input columns must be explicitly filled with zeros; output columns
696 // are populated by run() and don't need pre-filling
697 qCDebug(zero) << "Increasing the simulation length.";
698 const int oldLength = m_model->columnCount();
699 m_model->setColumnCount(simLength);
700 DolphinEdits::growInputColumns(*m_model, m_inputPorts, oldLength, simLength);
701
702 m_edited = true;
703 qCDebug(zero) << "Running simulation.";
704
705 if (runSimulation) {
706 run();
707 }
708}
709
710void BewavedDolphin::on_actionZoomOut_triggered()
711{
712 Application::guardedSlot(this, [this] {
713 sentryBreadcrumb("waveform", QStringLiteral("Zoom out"));
714 m_zoom->zoomOut();
715 zoomChanged();
716 });
717}
718
719void BewavedDolphin::on_actionZoomIn_triggered()
720{
721 Application::guardedSlot(this, [this] {
722 sentryBreadcrumb("waveform", QStringLiteral("Zoom in"));
723 m_zoom->zoomIn();
724 zoomChanged();
725 });
726}
727
728void BewavedDolphin::on_actionResetZoom_triggered()
729{
730 Application::guardedSlot(this, [this] {
731 sentryBreadcrumb("waveform", QStringLiteral("Zoom reset"));
732 m_zoom->reset();
733 zoomChanged();
734 });
735}
736
737void BewavedDolphin::zoomChanged()
738{
739 m_ui->actionZoomIn->setEnabled(m_zoom->canZoomIn());
740 m_ui->actionZoomOut->setEnabled(m_zoom->canZoomOut());
741}
742
743void BewavedDolphin::on_actionFitScreen_triggered()
744{
745 Application::guardedSlot(this, [this] {
746 sentryBreadcrumb("waveform", QStringLiteral("Fit screen"));
747 m_zoom->fitScreen();
748 zoomChanged();
749 });
750}
751
752void BewavedDolphin::on_actionClear_triggered()
753{
754 Application::guardedSlot(this, [this] {
755 sentryBreadcrumb("waveform", QStringLiteral("Clear input"));
756 const auto [cells, oldValues] = snapshotCells(allCellIndexes(m_inputPorts, m_model->columnCount()));
757
758 DolphinEdits::clearInputs(*m_model, m_inputPorts);
759
760 qCDebug(zero) << "Running simulation.";
761 m_undoStack.push(new SetCellsCommand(m_model, cells, oldValues, [this] {
762 m_edited = true;
763 run();
764 }));
765 });
766}
767
768void BewavedDolphin::on_actionAutoCrop_triggered()
769{
770 Application::guardedSlot(this, [this] {
771 sentryBreadcrumb("waveform", QStringLiteral("Auto crop"));
772 const int lastNonZero = DolphinEdits::lastNonZeroColumn(*m_model, m_inputPorts);
773 setLength(lastNonZero + 1, true);
774 });
775}
776
777void BewavedDolphin::on_actionCopy_triggered()
778{
779 Application::guardedSlot(this, [this] {
780 sentryBreadcrumb("clipboard", QStringLiteral("Waveform copy"));
781 const auto ranges = m_signalTableView->selectionModel()->selection();
782
783 if (ranges.isEmpty()) {
784 QApplication::clipboard()->clear();
785 return;
786 }
787
788 DolphinClipboard::copyToClipboard(*m_model, ranges);
789 });
790}
791
792void BewavedDolphin::on_actionCut_triggered()
793{
794 Application::guardedSlot(this, [this] {
795 sentryBreadcrumb("clipboard", QStringLiteral("Waveform cut"));
796 const auto ranges = m_signalTableView->selectionModel()->selection();
797
798 if (ranges.isEmpty()) {
799 QApplication::clipboard()->clear();
800 return;
801 }
802
803 // Cut = copy the selection, then clear it (which re-runs the simulation).
804 DolphinClipboard::copyToClipboard(*m_model, ranges);
805 on_actionSetTo0_triggered();
806 m_edited = true;
807 });
808}
809
810void BewavedDolphin::on_actionPaste_triggered()
811{
812 Application::guardedSlot(this, [this] {
813 sentryBreadcrumb("clipboard", QStringLiteral("Waveform paste"));
814 const auto ranges = m_signalTableView->selectionModel()->selection();
815
816 if (ranges.isEmpty()) {
817 return;
818 }
819
820 if (DolphinClipboard::pasteFromClipboard(*m_model, ranges)) {
821 m_edited = true;
822 run();
823 }
824 });
825}
826
827void BewavedDolphin::on_actionSave_triggered()
828{
829 Application::guardedSlot(this, [this] {
830 sentryBreadcrumb("file", QStringLiteral("Waveform save"));
831 if (m_currentFile.fileName().isEmpty()) {
832 on_actionSaveAs_triggered();
833 return;
834 }
835
836 DolphinFile::save(*m_model, m_currentFile.absoluteFilePath(), m_inputPorts);
837 m_ui->statusbar->showMessage(tr("Saved file successfully."), 4000);
838 m_edited = false;
839 });
840}
841
842void BewavedDolphin::on_actionSaveAs_triggered()
843{
844 Application::guardedSlot(this, [this] {
845 sentryBreadcrumb("file", QStringLiteral("Waveform save as"));
846
847 // List the format that matches the current file first so it is the default selection
848 const QString fileFilter = m_currentFile.fileName().endsWith(".csv") ?
849 tr("CSV files") + " (*.csv);;" + tr("Dolphin files") + " (*.dolphin);;" + tr("All supported files") + " (*.dolphin *.csv)"
850 : tr("Dolphin files") + " (*.dolphin);;" + tr("CSV files") + " (*.csv);;" + tr("All supported files") + " (*.dolphin *.csv)";
851
852 const QString initialPath = m_currentFile.fileName().isEmpty()
853 ? (m_host ? m_host->currentFile().absolutePath() : QString())
854 : m_currentFile.absoluteFilePath();
855
856 const auto result = FileDialogs::provider()->getSaveFileName(this, tr("Save File as..."), initialPath, fileFilter);
857 QString fileName = result.fileName;
858
859 if (fileName.isEmpty()) {
860 return;
861 }
862
863 // Append the correct extension when the user types a bare name without one,
864 // inferring the format from whichever filter was active in the dialog
865 if (!fileName.endsWith(".dolphin") && !fileName.endsWith(".csv")) {
866 if (result.selectedFilter.contains("dolphin")) {
867 fileName.append(".dolphin");
868 } else {
869 fileName.append(".csv");
870 }
871 }
872
873 DolphinFile::save(*m_model, fileName, m_inputPorts);
874 m_currentFile = QFileInfo(fileName);
875 associateToWiRedPanda(fileName);
876 setWindowTitle(tr("beWavedDolphin Simulator") + " [" + m_currentFile.fileName() + "]");
877 m_ui->statusbar->showMessage(tr("Saved file successfully."), 4000);
878 m_edited = false;
879 });
880}
881
882void BewavedDolphin::associateToWiRedPanda(const QString &fileName)
883{
884 // Without a host there is no wiRedPanda project file to link this waveform to.
885 if (!m_host) {
886 return;
887 }
888
889 // Only prompt when the file is new (not already linked) and we are in interactive mode;
890 // non-interactive (command-line / test) sessions skip the dialog entirely
891 if ((m_host->dolphinFileName() != fileName) && Application::interactiveMode) {
892 const auto reply =
893 QMessageBox::question(
894 this,
895 tr("wiRedPanda - beWavedDolphin"),
896 tr("Do you want to link this beWavedDolphin file to your current wiRedPanda file and save it?"),
897 QMessageBox::Yes | QMessageBox::No);
898
899 if (reply == QMessageBox::Yes) {
900 m_host->setDolphinFileName(fileName);
901 m_host->save({});
902 }
903 }
904}
905
906void BewavedDolphin::on_actionLoad_triggered()
907{
908 Application::guardedSlot(this, [this] {
909 sentryBreadcrumb("file", QStringLiteral("Waveform load dialog"));
910 QDir defaultDirectory;
911
912 // Prefer the last-used dolphin file's directory; fall back to the main window's
913 // working directory, and finally to the home directory
914 if (m_currentFile.exists()) {
915 defaultDirectory.setPath(m_currentFile.absolutePath());
916 } else if (m_host) {
917 if (m_host->currentFile().exists()) {
918 m_host->currentFile().dir();
919 } else {
920 defaultDirectory.setPath(QDir::homePath());
921 }
922 } else {
923 defaultDirectory.setPath(QDir::homePath());
924 }
925
926 const QString homeDir(m_host ? m_host->currentDir().absolutePath() : QDir::homePath());
927
928 const QString fileName = FileDialogs::provider()->getOpenFileName(
929 this, tr("Open File"), homeDir,
930 tr("All supported files") + " (*.dolphin *.csv);;" + tr("Dolphin files") + " (*.dolphin);;" + tr("CSV files") + " (*.csv)");
931
932 if (fileName.isEmpty()) {
933 return;
934 }
935
936 load(fileName);
937 m_edited = false;
938 m_ui->statusbar->showMessage(tr("File loaded successfully."), 4000);
939 });
940}
941
942void BewavedDolphin::load(const QString &fileName)
943{
944 // DolphinFile handles the on-disk format; we apply the parsed input rows and
945 // record the association with the circuit file.
946 applyWaveformData(DolphinFile::load(fileName, m_inputPorts));
947 m_currentFile = QFileInfo(fileName);
948 associateToWiRedPanda(fileName);
949 setWindowTitle(tr("beWavedDolphin Simulator") + " [" + m_currentFile.fileName() + "]");
950}
951
952void BewavedDolphin::applyWaveformData(const DolphinSerializer::WaveformData &fileData)
953{
954 setLength(fileData.columns, false);
955 qCDebug(zero) << "Update table.";
956
957 {
958 SignalModel::BulkEditGuard guard(*m_model);
959 for (int row = 0; row < fileData.inputPorts; ++row) {
960 for (int col = 0; col < fileData.columns; ++col) {
961 m_model->setValue(row, col, fileData.values[row * fileData.columns + col]);
962 }
963 }
964 }
965
966 run();
967}
968
969void BewavedDolphin::on_actionShowNumbers_triggered()
970{
971 Application::guardedSlot(this, [this] {
972 sentryBreadcrumb("waveform", QStringLiteral("Show numbers"));
973 // Display mode is a pure view concern now: the model keeps the same values and the
974 // delegate switches between numeric text and waveform rendering.
975 m_delegate->setPlotType(PlotType::Number);
976 m_signalTableView->viewport()->update();
977 });
978}
979
980void BewavedDolphin::on_actionShowWaveforms_triggered()
981{
982 Application::guardedSlot(this, [this] {
983 sentryBreadcrumb("waveform", QStringLiteral("Show waveforms"));
984 m_delegate->setPlotType(PlotType::Line);
985 m_signalTableView->viewport()->update();
986 });
987}
988
989void BewavedDolphin::on_actionExportToPng_triggered()
990{
991 Application::guardedSlot(this, [this] {
992 sentryBreadcrumb("export", QStringLiteral("Waveform export PNG"));
993 QString pngFile = FileDialogs::provider()->getSaveFileName(this, tr("Export to Image"), m_currentFile.absolutePath(), tr("PNG files") + " (*.png)").fileName;
994
995 if (pngFile.isEmpty()) {
996 return;
997 }
998
999 if (!pngFile.endsWith(".png", Qt::CaseInsensitive)) {
1000 pngFile.append(".png");
1001 }
1002
1003 DolphinExporter::exportToPng(m_model, m_delegate->plotType(), pngFile);
1004 });
1005}
1006
1007void BewavedDolphin::on_actionExportToPdf_triggered()
1008{
1009 Application::guardedSlot(this, [this] {
1010 sentryBreadcrumb("export", QStringLiteral("Waveform export PDF"));
1011 QString pdfFile = FileDialogs::provider()->getSaveFileName(this, tr("Export to PDF"), m_currentFile.absolutePath(), tr("PDF files") + " (*.pdf)").fileName;
1012
1013 if (pdfFile.isEmpty()) {
1014 return;
1015 }
1016
1017 if (!pdfFile.endsWith(".pdf", Qt::CaseInsensitive)) {
1018 pdfFile.append(".pdf");
1019 }
1020
1021 DolphinExporter::exportToPdf(m_model, m_delegate->plotType(), pdfFile);
1022 });
1023}
1024
1025void BewavedDolphin::on_actionAbout_triggered()
1026{
1027 Application::guardedSlot(this, [this] {
1028 QMessageBox::about(this,
1029 "beWavedDolphin",
1030 tr("<p>beWavedDolphin is a waveform simulator for wiRedPanda, developed by the Federal University of São Paulo"
1031 " to help students learn about logic circuits.</p>"
1032 "<p>Software version: %1</p>"
1033 "<p><strong>Creators:</strong></p>"
1034 "<ul>"
1035 "<li> Prof. Fábio Cappabianco, Ph.D. </li>"
1036 "</ul>"
1037 "<p> beWavedDolphin is currently maintained by Prof. Fábio Cappabianco, Ph.D. and his students.</p>"
1038 "<p> Please file a report at our GitHub page if you find a bug or want to request a new feature.</p>"
1039 "<p><a href=\"http://gibis-unifesp.github.io/wiRedPanda/\">Visit our website!</a></p>")
1040 .arg(QApplication::applicationVersion()));
1041 });
1042}
1043
1044void BewavedDolphin::on_actionAboutQt_triggered()
1045{
1046 Application::guardedSlot(this, [this] {
1047 QMessageBox::aboutQt(this);
1048 });
1049}
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:81
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:37
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.