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([&] {
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([&] {
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 {
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 {
136 QTabWidget *tabWidget = m_mainWindow->findChild<QTabWidget *>("tab");
137 if (!tabWidget) {
138 return createErrorResponse("Tab widget not found", requestId, JsonRpcError::InternalError);
139 }
140
141 if (tabWidget->count() == 0) {
142 return createErrorResponse("No tabs to close", requestId, JsonRpcError::OperationFailed);
143 }
144
145 Scene *scene = currentScene();
146 if (scene && scene->simulation()) {
147 scene->simulation()->stop();
148 }
149
150 int currentIndex = tabWidget->currentIndex();
151
152 QWidget *tabWidget_widget = tabWidget->widget(currentIndex);
153 tabWidget->removeTab(currentIndex);
154 if (tabWidget_widget) {
155 tabWidget_widget->deleteLater();
156 }
157
158 return createSuccessResponse(QJsonObject(), requestId);
159 }, "close circuit", requestId);
160}
161
162QJsonObject FileHandler::handleGetTabCount(const QJsonObject &, const QJsonValue &requestId)
163{
164 if (!m_mainWindow) {
165 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
166 }
167
168 return tryCommand([&]() -> QJsonObject {
169 QTabWidget *tabWidget = m_mainWindow->findChild<QTabWidget *>("tab");
170 if (!tabWidget) {
171 return createErrorResponse("Tab widget not found", requestId, JsonRpcError::InternalError);
172 }
173
174 QJsonObject result;
175 result["tab_count"] = tabWidget->count();
176
177 return createSuccessResponse(result, requestId);
178 }, "get tab count", requestId);
179}
180
181QJsonObject FileHandler::handleExportImage(const QJsonObject &params, const QJsonValue &requestId)
182{
183 if (!validateParameters(params, {"filename", "format"})) {
184 return createErrorResponse("Missing required parameters: filename, format", requestId, JsonRpcError::InvalidParams);
185 }
186
187 QString errorMsg;
188 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
189 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
190 }
191
192 if (!validateNonEmptyString(params.value("format"), "format", errorMsg)) {
193 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
194 }
195
196 QString filename = params.value("filename").toString();
197 QString format = params.value("format").toString().toLower();
198 const int padding = std::clamp(params.value("padding").toInt(20), 0, kMaxExportPadding);
199
200 Scene *scene = currentScene();
201 if (!scene) {
202 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
203 }
204
205 if (format != "png" && format != "svg" && format != "pdf") {
206 return createErrorResponse("Unsupported format. Use 'png', 'svg', or 'pdf'", requestId, JsonRpcError::ValidationError);
207 }
208
209 return tryCommand([&]() -> QJsonObject {
210 QRectF sceneRect = scene->itemsBoundingRect();
211 if (sceneRect.isEmpty()) {
212 return createErrorResponse("Scene is empty - nothing to export", requestId, JsonRpcError::OperationFailed);
213 }
214
215 sceneRect.adjust(-padding, -padding, padding, padding);
216
217 if (format == "svg") {
218 QSvgGenerator generator;
219 generator.setFileName(filename);
220 generator.setSize(sceneRect.size().toSize());
221 generator.setViewBox(sceneRect);
222 generator.setTitle("wiRedPanda Circuit");
223 generator.setDescription("Circuit exported from wiRedPanda");
224
225 QPainter painter(&generator);
226 if (!painter.isActive()) {
227 return createErrorResponse(QString("Failed to open SVG file for writing: %1").arg(filename),
228 requestId, JsonRpcError::FileError);
229 }
230 scene->render(&painter, QRectF(), sceneRect);
231 painter.end();
232
233 const QFileInfo svgInfo(filename);
234 if (!svgInfo.exists() || svgInfo.size() == 0) {
235 return createErrorResponse("SVG export failed: file was not written", requestId, JsonRpcError::FileError);
236 }
237 } else if (format == "pdf") {
238 QPdfWriter pdfWriter(filename);
239 pdfWriter.setTitle("wiRedPanda Circuit");
240 pdfWriter.setCreator("wiRedPanda");
241 // Map scene pixels 1:1 to PDF points (72 DPI) and size the page to the bounding box.
242 pdfWriter.setResolution(72);
243 pdfWriter.setPageSize(QPageSize(sceneRect.size(), QPageSize::Point));
244 pdfWriter.setPageMargins(QMarginsF(0, 0, 0, 0));
245
246 QPainter painter(&pdfWriter);
247 if (!painter.isActive()) {
248 return createErrorResponse(QString("Failed to open PDF file for writing: %1").arg(filename),
249 requestId, JsonRpcError::FileError);
250 }
251 painter.setRenderHint(QPainter::Antialiasing);
252 scene->render(&painter, QRectF(), sceneRect);
253 painter.end();
254 } else { // png
255 // Delegates to CircuitExporter's already-hardened renderer instead of sizing an
256 // image directly from sceneRect: that bounding/scale-to-fit logic (capped at
257 // CircuitExporter::kMaxImageDimension) exists in exactly one place so it can't drift
258 // out of sync between the GUI's "Export to Image" action and this MCP command.
259 const QImage image = CircuitExporter::renderScaledImage(scene, sceneRect);
260
261 if (!image.save(filename, "PNG")) {
262 return createErrorResponse("Failed to save PNG file", requestId, JsonRpcError::FileError);
263 }
264
265 QJsonObject result;
266 result["exported_file"] = filename;
267 result["format"] = format;
268 result["size"] = QString("%1x%2").arg(image.width()).arg(image.height());
269
270 return createSuccessResponse(result, requestId);
271 }
272
273 QJsonObject result;
274 result["exported_file"] = filename;
275 result["format"] = format;
276 result["size"] = QString("%1x%2").arg(sceneRect.width()).arg(sceneRect.height());
277
278 return createSuccessResponse(result, requestId);
279 }, "export image", requestId);
280}
281
282QJsonObject FileHandler::handleExportArduino(const QJsonObject &params, const QJsonValue &requestId)
283{
284 if (!validateParameters(params, {"filename"})) {
285 return createErrorResponse("Missing required parameter: filename", requestId, JsonRpcError::InvalidParams);
286 }
287
288 QString errorMsg;
289 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
290 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
291 }
292
293 QString filename = params.value("filename").toString();
294
295 if (!m_mainWindow) {
296 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
297 }
298
299 return tryCommand([&]() -> QJsonObject {
300 m_mainWindow->exportToArduino(filename);
301
302 QJsonObject result;
303 result["exported_file"] = filename;
304 result["format"] = "arduino";
305
306 return createSuccessResponse(result, requestId);
307 }, "export Arduino code", requestId);
308}
309
310QJsonObject FileHandler::handleExportSystemVerilog(const QJsonObject &params, const QJsonValue &requestId)
311{
312 if (!validateParameters(params, {"filename"})) {
313 return createErrorResponse("Missing required parameter: filename", requestId, JsonRpcError::InvalidParams);
314 }
315
316 QString errorMsg;
317 if (!validateNonEmptyString(params.value("filename"), "filename", errorMsg)) {
318 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
319 }
320
321 QString filename = params.value("filename").toString();
322
323 if (!m_mainWindow) {
324 return createErrorResponse("No main window available", requestId, JsonRpcError::InternalError);
325 }
326
327 return tryCommand([&]() -> QJsonObject {
328 m_mainWindow->exportToSystemVerilog(filename);
329
330 QJsonObject result;
331 result["exported_file"] = filename;
332 result["format"] = "systemverilog";
333
334 return createSuccessResponse(result, requestId);
335 }, "export SystemVerilog code", requestId);
336}
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:47
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.).