wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
FileHandler.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
8#include <QDir>
9#include <QFile>
10#include <QFileInfo>
11#include <QImage>
12#include <QPageSize>
13#include <QPainter>
14#include <QPdfWriter>
15#include <QSvgGenerator>
16#include <QTabWidget>
17
18#include "App/Scene/Scene.h"
19#include "App/Scene/Workspace.h"
21#include "App/UI/MainWindow.h"
22
23namespace {
24
28constexpr int kMaxExportPadding = 2000;
29
30} // namespace
31
32FileHandler::FileHandler(MainWindow *mainWindow, const MCPValidator *validator)
33 : BaseHandler(mainWindow, validator)
34{
35}
36
37QJsonObject FileHandler::handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId)
38{
39 if (command == "load_circuit") {
40 return handleLoadCircuit(params, requestId);
41 } else if (command == "save_circuit") {
42 return handleSaveCircuit(params, requestId);
43 } else if (command == "new_circuit") {
44 return handleNewCircuit(params, requestId);
45 } else if (command == "close_circuit") {
46 return handleCloseCircuit(params, requestId);
47 } else if (command == "get_tab_count") {
48 return handleGetTabCount(params, requestId);
49 } else if (command == "export_image") {
50 return handleExportImage(params, requestId);
51 } else if (command == "export_arduino") {
52 return handleExportArduino(params, requestId);
53 } else if (command == "export_systemverilog") {
54 return handleExportSystemVerilog(params, requestId);
55 } else {
56 return createErrorResponse(QString("Unknown file command: %1").arg(command),
58 }
59}
60
61QJsonObject FileHandler::handleLoadCircuit(const QJsonObject &params, const QJsonValue &requestId)
62{
63 if (!validateParameters(params, {"filename"})) {
64 return createErrorResponse("Missing required parameter: filename", requestId, JsonRpcError::InvalidParams);
65 }
66
67 QString errorMsg;
68 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
69 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
70 }
71
72 QString filename = params.value("filename").toString();
73
74 if (!m_mainWindow) {
75 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
76 }
77
78 return tryCommand([&] { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
79 m_mainWindow->loadPandaFile(filename);
80 return createSuccessResponse(QJsonObject(), requestId);
81 }, "load circuit", requestId);
82}
83
84QJsonObject FileHandler::handleSaveCircuit(const QJsonObject &params, const QJsonValue &requestId)
85{
86 if (!validateParameters(params, {"filename"})) {
87 return createErrorResponse("Missing required parameter: filename", requestId, JsonRpcError::InvalidParams);
88 }
89
90 QString errorMsg;
91 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
92 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
93 }
94
95 QString filename = params.value("filename").toString();
96
97 if (!filename.endsWith(".panda", Qt::CaseInsensitive)) {
98 return createErrorResponse("Invalid file extension. Use .panda for circuit files", requestId, JsonRpcError::ValidationError);
99 }
100
101 if (!m_mainWindow) {
102 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
103 }
104
105 return tryCommand([&] { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
106 m_mainWindow->save(filename);
107 return createSuccessResponse(QJsonObject(), requestId);
108 }, "save circuit", requestId);
109}
110
111QJsonObject FileHandler::handleNewCircuit(const QJsonObject &, const QJsonValue &requestId)
112{
113 if (!m_mainWindow) {
114 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
115 }
116
117 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
118 m_mainWindow->createNewTab();
119
120 Scene *scene = currentScene();
121 if (scene && scene->simulation()) {
122 scene->simulation()->stop();
123 }
124
125 return createSuccessResponse(QJsonObject(), requestId);
126 }, "create new circuit", requestId);
127}
128
129QJsonObject FileHandler::handleCloseCircuit(const QJsonObject &, const QJsonValue &requestId)
130{
131 if (!m_mainWindow) {
132 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
133 }
134
135 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
136 QTabWidget *tabWidget = m_mainWindow->findChild<QTabWidget *>("tab");
137 // Unreachable: MainWindowUi::setupUi() unconditionally creates the "tab" QTabWidget;
138 // a real MainWindow can never be missing it.
139 if (!tabWidget) {
140 return createErrorResponse("Tab widget not found", requestId, JsonRpcError::InternalError); // LCOV_EXCL_LINE
141 }
142
143 if (tabWidget->count() == 0) {
144 return createErrorResponse("No tabs to close", requestId, JsonRpcError::OperationFailed);
145 }
146
147 Scene *scene = currentScene();
148 if (scene && scene->simulation()) {
149 scene->simulation()->stop();
150 }
151
152 int currentIndex = tabWidget->currentIndex();
153
154 QWidget *tabWidget_widget = tabWidget->widget(currentIndex);
155 tabWidget->removeTab(currentIndex);
156 if (tabWidget_widget) {
157 tabWidget_widget->deleteLater();
158 }
159
160 return createSuccessResponse(QJsonObject(), requestId);
161 }, "close circuit", requestId);
162}
163
164QJsonObject FileHandler::handleGetTabCount(const QJsonObject &, const QJsonValue &requestId)
165{
166 if (!m_mainWindow) {
167 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
168 }
169
170 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
171 QTabWidget *tabWidget = m_mainWindow->findChild<QTabWidget *>("tab");
172 // Unreachable: same "setupUi() always creates it" invariant as handleCloseCircuit().
173 if (!tabWidget) {
174 return createErrorResponse("Tab widget not found", requestId, JsonRpcError::InternalError); // LCOV_EXCL_LINE
175 }
176
177 QJsonObject result;
178 result["tab_count"] = tabWidget->count();
179
180 return createSuccessResponse(result, requestId);
181 }, "get tab count", requestId);
182}
183
184QJsonObject FileHandler::handleExportImage(const QJsonObject &params, const QJsonValue &requestId)
185{
186 if (!validateParameters(params, {"filename", "format"})) {
187 return createErrorResponse("Missing required parameters: filename, format", requestId, JsonRpcError::InvalidParams);
188 }
189
190 QString errorMsg;
191 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
192 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
193 }
194
195 if (!validateNonEmptyString(params.value("format"), "format", errorMsg)) {
196 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
197 }
198
199 QString filename = params.value("filename").toString();
200 QString format = params.value("format").toString().toLower();
201 const int padding = std::clamp(params.value("padding").toInt(20), 0, kMaxExportPadding);
202
203 Scene *scene = currentScene();
204 if (!scene) {
205 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
206 }
207
208 if (format != "png" && format != "svg" && format != "pdf") {
209 return createErrorResponse("Unsupported format. Use 'png', 'svg', or 'pdf'", requestId, JsonRpcError::ValidationError);
210 }
211
212 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
213 QRectF sceneRect = scene->itemsBoundingRect();
214 // Unreachable: a real Scene always contains a permanent, non-empty rubber-band
215 // selection-rect item (SceneInteraction's m_selectionRect, unconditionally added),
216 // so itemsBoundingRect() is never empty for any Scene constructed the normal way
217 // (same invariant as MinimapWidget.cpp's identical finding).
218 if (sceneRect.isEmpty()) {
219 return createErrorResponse("Scene is empty - nothing to export", requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
220 }
221
222 sceneRect.adjust(-padding, -padding, padding, padding);
223
224 if (format == "svg") {
225 QSvgGenerator generator;
226 generator.setFileName(filename);
227 generator.setSize(sceneRect.size().toSize());
228 generator.setViewBox(sceneRect);
229 generator.setTitle("wiRedPanda Circuit");
230 generator.setDescription("Circuit exported from wiRedPanda");
231
232 QPainter painter(&generator);
233 if (!painter.isActive()) {
234 return createErrorResponse(QString("Failed to open SVG file for writing: %1").arg(filename),
235 requestId, JsonRpcError::FileError);
236 }
237 scene->render(&painter, QRectF(), sceneRect);
238 painter.end();
239
240 const QFileInfo svgInfo(filename);
241 if (!svgInfo.exists() || svgInfo.size() == 0) {
242 return createErrorResponse("SVG export failed: file was not written", requestId, JsonRpcError::FileError);
243 }
244 } else if (format == "pdf") {
245 QPdfWriter pdfWriter(filename);
246 pdfWriter.setTitle("wiRedPanda Circuit");
247 pdfWriter.setCreator("wiRedPanda");
248 // Map scene pixels 1:1 to PDF points (72 DPI) and size the page to the bounding box.
249 pdfWriter.setResolution(72);
250 pdfWriter.setPageSize(QPageSize(sceneRect.size(), QPageSize::Point));
251 pdfWriter.setPageMargins(QMarginsF(0, 0, 0, 0));
252
253 QPainter painter(&pdfWriter);
254 if (!painter.isActive()) {
255 return createErrorResponse(QString("Failed to open PDF file for writing: %1").arg(filename),
256 requestId, JsonRpcError::FileError);
257 }
258 painter.setRenderHint(QPainter::Antialiasing);
259 scene->render(&painter, QRectF(), sceneRect);
260 painter.end();
261 } else { // png
262 // Delegates to CircuitExporter's already-hardened renderer instead of sizing an
263 // image directly from sceneRect: that bounding/scale-to-fit logic (capped at
264 // CircuitExporter::kMaxImageDimension) exists in exactly one place so it can't drift
265 // out of sync between the GUI's "Export to Image" action and this MCP command.
266 const QImage image = CircuitExporter::renderScaledImage(scene, sceneRect);
267
268 if (!image.save(filename, "PNG")) {
269 return createErrorResponse("Failed to save PNG file", requestId, JsonRpcError::FileError);
270 }
271
272 QJsonObject result;
273 result["exported_file"] = filename;
274 result["format"] = format;
275 result["size"] = QString("%1x%2").arg(image.width()).arg(image.height());
276
277 return createSuccessResponse(result, requestId);
278 }
279
280 QJsonObject result;
281 result["exported_file"] = filename;
282 result["format"] = format;
283 result["size"] = QString("%1x%2").arg(sceneRect.width()).arg(sceneRect.height());
284
285 return createSuccessResponse(result, requestId);
286 }, "export image", requestId);
287}
288
289QJsonObject FileHandler::handleExportArduino(const QJsonObject &params, const QJsonValue &requestId)
290{
291 if (!validateParameters(params, {"filename"})) {
292 return createErrorResponse("Missing required parameter: filename", requestId, JsonRpcError::InvalidParams);
293 }
294
295 QString errorMsg;
296 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
297 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
298 }
299
300 QString filename = params.value("filename").toString();
301
302 if (!m_mainWindow) {
303 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
304 }
305
306 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
307 m_mainWindow->exportToArduino(filename);
308
309 QJsonObject result;
310 result["exported_file"] = filename;
311 result["format"] = "arduino";
312
313 return createSuccessResponse(result, requestId);
314 }, "export Arduino code", requestId);
315}
316
317QJsonObject FileHandler::handleExportSystemVerilog(const QJsonObject &params, const QJsonValue &requestId)
318{
319 if (!validateParameters(params, {"filename"})) {
320 return createErrorResponse("Missing required parameter: filename", requestId, JsonRpcError::InvalidParams);
321 }
322
323 QString errorMsg;
324 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
325 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
326 }
327
328 QString filename = params.value("filename").toString();
329
330 if (!m_mainWindow) {
331 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
332 }
333
334 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
335 m_mainWindow->exportToSystemVerilog(filename);
336
337 QJsonObject result;
338 result["exported_file"] = filename;
339 result["format"] = "systemverilog";
340
341 return createSuccessResponse(result, requestId);
342 }, "export SystemVerilog code", requestId);
343}
CircuitExporter: render a Scene to PDF or PNG image.
Main application window providing menus, toolbars, and tab management.
Main circuit editing scene with undo/redo and user interaction.
WorkSpace widget: the complete circuit editing environment for one tab.
QJsonObject createSuccessResponse(const QJsonObject &result={}, const QJsonValue &requestId=QJsonValue()) const
QJsonObject tryCommand(Fn &&fn, const QString &action, const QJsonValue &requestId=QJsonValue())
Wraps fn in a try/catch, returning an error response on exception.
Definition BaseHandler.h:55
BaseHandler(MainWindow *mainWindow, const MCPValidator *validator)
QJsonObject createErrorResponse(const QString &error, const QJsonValue &requestId=QJsonValue(), int code=JsonRpcError::InternalError) const
MainWindow * m_mainWindow
Definition BaseHandler.h:85
bool validateParameters(const QJsonObject &params, const QStringList &required) const
bool validateNonEmptyString(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
Scene * currentScene()
FileHandler(MainWindow *mainWindow, const MCPValidator *validator)
QJsonObject handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId) override
JSON Schema validator for MCP commands and responses using native json-schema-validator.
The top-level application window hosting the tab bar, menus, element palette, and editor.
Definition MainWindow.h:46
Simulation * simulation()
Returns the simulation engine associated with this scene.
Definition Scene.cpp:288
void stop()
Stops the simulation timer.
QImage renderScaledImage(Scene *scene, const QRectF &paddedRect)
Renders scene into a transparent-filled QImage bounded by kMaxImageDimension.
constexpr int SceneNotAvailable
No active circuit scene to operate on.
constexpr int MethodNotFound
The requested method does not exist or is unavailable.
constexpr int OperationFailed
Generic Qt API / handler operation failure (catch-all for tryCommand).
constexpr int InternalError
Internal JSON-RPC error (last-resort).
constexpr int FileError
File save/load/path error.
constexpr int ValidationError
Semantic validation failure (e.g. port index out of range, enum value not allowed).
constexpr int InvalidParams
Invalid method parameters (missing required, wrong type, etc.).