1 /*
2 * OpenSCAD (www.openscad.org)
3 * Copyright (C) 2009-2011 Clifford Wolf <clifford@clifford.at> and
4 * Marius Kintel <marius@kintel.net>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * As a special exception, you have permission to link this program
12 * with the CGAL library and distribute executables, as long as you
13 * follow the requirements of the GNU GPL in regard to all of the
14 * software in the executable aside from CGAL.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 *
25 */
26 #include <iostream>
27 #include "boost-utils.h"
28 #include "comment.h"
29 #include "openscad.h"
30 #include "GeometryCache.h"
31 #include "ModuleCache.h"
32 #include "MainWindow.h"
33 #include "OpenSCADApp.h"
34 #include "parsersettings.h"
35 #include "rendersettings.h"
36 #include "Preferences.h"
37 #include "printutils.h"
38 #include "node.h"
39 #include "csgnode.h"
40 #include "builtin.h"
41 #include "memory.h"
42 #include "expression.h"
43 #include "modcontext.h"
44 #include "progress.h"
45 #include "dxfdim.h"
46 #include "settings.h"
47 #include "AboutDialog.h"
48 #include "FontListDialog.h"
49 #include "LibraryInfoDialog.h"
50 #include "RenderStatistic.h"
51 #include "scintillaeditor.h"
52 #ifdef ENABLE_OPENCSG
53 #include "CSGTreeEvaluator.h"
54 #include "OpenCSGRenderer.h"
55 #include <opencsg.h>
56 #endif
57 #include "ProgressWidget.h"
58 #include "ThrownTogetherRenderer.h"
59 #include "CSGTreeNormalizer.h"
60 #include "QGLView.h"
61 #include "mouseselector.h"
62 #ifdef Q_OS_MAC
63 #include "CocoaUtils.h"
64 #endif
65 #include "PlatformUtils.h"
66 #ifdef OPENSCAD_UPDATER
67 #include "AutoUpdater.h"
68 #endif
69 #include "tabmanager.h"
70
71 #include <QMenu>
72 #include <QTime>
73 #include <QMenuBar>
74 #include <QSplitter>
75 #include <QFileDialog>
76 #include <QHBoxLayout>
77 #include <QVBoxLayout>
78 #include <QLabel>
79 #include <QFileInfo>
80 #include <QTextStream>
81 #include <QStatusBar>
82 #include <QDropEvent>
83 #include <QMimeData>
84 #include <QUrl>
85 #include <QTimer>
86 #include <QMessageBox>
87 #include <QDesktopServices>
88 #include <QProgressDialog>
89 #include <QMutexLocker>
90 #include <QTemporaryFile>
91 #include <QDockWidget>
92 #include <QClipboard>
93 #include <QDesktopWidget>
94 #include <string>
95 #include "QWordSearchField.h"
96 #include <QSettings> //Include QSettings for direct operations on settings arrays
97 #include "QSettingsCached.h"
98 #include <QSound>
99
100 #define ENABLE_3D_PRINTING
101 #include "OctoPrint.h"
102 #include "PrintService.h"
103
104 #include <fstream>
105
106 #include <algorithm>
107 #include <boost/version.hpp>
108 #include <sys/stat.h>
109
110 #ifdef ENABLE_CGAL
111
112 #include "CGALCache.h"
113 #include "GeometryEvaluator.h"
114 #include "CGALRenderer.h"
115 #include "CGAL_Nef_polyhedron.h"
116 #include "cgal.h"
117 #include "cgalworker.h"
118 #include "cgalutils.h"
119
120 #endif // ENABLE_CGAL
121
122 #include "FontCache.h"
123 #include "PrintInitDialog.h"
124 #include "input/InputDriverManager.h"
125 #include <cstdio>
126 #include <memory>
127 #include <QtNetwork>
128
129 static const int autoReloadPollingPeriodMS = 200;
130
131 // Global application state
132 unsigned int GuiLocker::gui_locked = 0;
133
134 static char copyrighttext[] =
135 "<p>Copyright (C) 2009-2021 The OpenSCAD Developers</p>"
136 "<p>This program is free software; you can redistribute it and/or modify "
137 "it under the terms of the GNU General Public License as published by "
138 "the Free Software Foundation; either version 2 of the License, or "
139 "(at your option) any later version.<p>";
140 bool MainWindow::undockMode = false;
141 bool MainWindow::reorderMode = false;
142 const int MainWindow::tabStopWidth = 15;
143 QElapsedTimer *MainWindow::progressThrottle = new QElapsedTimer();
144
145 namespace {
146
147 struct DockFocus {
148 QWidget *widget;
149 std::function<void(MainWindow *)> focus;
150 };
151
findAction(const QList<QAction * > & actions,const std::string & name)152 QAction *findAction(const QList<QAction *> &actions, const std::string &name)
153 {
154 for (const auto action : actions) {
155 if (action->objectName().toStdString() == name) {
156 return action;
157 }
158 if (action->menu()) {
159 auto foundAction = findAction(action->menu()->actions(), name);
160 if (foundAction) return foundAction;
161 }
162 }
163 return nullptr;
164 }
165
htmlEscape(const QString & str)166 const QString htmlEscape(const QString& str) {
167 return str.toHtmlEscaped();
168 }
169
htmlEscape(const std::string & str)170 const QString htmlEscape(const std::string& str) {
171 return htmlEscape(QString::fromStdString(str));
172 }
173
fileExportedMessage(const char * format,const QString & filename)174 void fileExportedMessage(const char *format, const QString &filename) {
175 LOG(message_group::None,Location::NONE,"","%1$s export finished: %2$s",format,filename.toUtf8().constData());
176 }
177
178 } // namespace
179
MainWindow(const QStringList & filenames)180 MainWindow::MainWindow(const QStringList &filenames)
181 : top_ctx(Context::create<BuiltinContext>()), root_inst("group"), library_info_dialog(nullptr), font_list_dialog(nullptr),
182 procevents(false), tempFile(nullptr), progresswidget(nullptr), includes_mtime(0), deps_mtime(0), last_parser_error_pos(-1)
183 {
184 setupUi(this);
185
186 consoleUpdater = new QTimer(this);
187 consoleUpdater->setSingleShot(true);
188 connect(consoleUpdater, SIGNAL(timeout()), this->console, SLOT(update()));
189
190 editorDockTitleWidget = new QWidget();
191 consoleDockTitleWidget = new QWidget();
192 parameterDockTitleWidget = new QWidget();
193 errorLogDockTitleWidget = new QWidget();
194
195 // actions not included in menu
196 this->addAction(editActionInsertTemplate);
197
198 this->editorDock->setConfigKey("view/hideEditor");
199 this->editorDock->setAction(this->windowActionHideEditor);
200 this->consoleDock->setConfigKey("view/hideConsole");
201 this->consoleDock->setAction(this->windowActionHideConsole);
202 this->parameterDock->setConfigKey("view/hideCustomizer");
203 this->parameterDock->setAction(this->windowActionHideCustomizer);
204 this->errorLogDock->setConfigKey("view/hideErrorLog");
205 this->errorLogDock->setAction(this->windowActionHideErrorLog);
206
207 this->versionLabel = nullptr; // must be initialized before calling updateStatusBar()
208 updateStatusBar(nullptr);
209
210 const QString importStatement = "import(\"%1\");\n";
211 const QString surfaceStatement = "surface(\"%1\");\n";
212 knownFileExtensions["stl"] = importStatement;
213 knownFileExtensions["3mf"] = importStatement;
214 knownFileExtensions["off"] = importStatement;
215 knownFileExtensions["dxf"] = importStatement;
216 knownFileExtensions["svg"] = importStatement;
217 knownFileExtensions["amf"] = importStatement;
218 knownFileExtensions["dat"] = surfaceStatement;
219 knownFileExtensions["png"] = surfaceStatement;
220 knownFileExtensions["scad"] = "";
221 knownFileExtensions["csg"] = "";
222
223 root_module = nullptr;
224 parsed_module = nullptr;
225 absolute_root_node = nullptr;
226
227 // Open Recent
228 for (int i = 0; i<UIUtils::maxRecentFiles; ++i) {
229 this->actionRecentFile[i] = new QAction(this);
230 this->actionRecentFile[i]->setVisible(false);
231 this->menuOpenRecent->addAction(this->actionRecentFile[i]);
232 connect(this->actionRecentFile[i], SIGNAL(triggered()),
233 this, SLOT(actionOpenRecent()));
234 }
235
236 // Preferences initialization happens on first tab creation, and depends on colorschemes from editor.
237 // Any code dependent on Preferences must come after the TabManager instantiation
238 tabManager = new TabManager(this, filenames.isEmpty() ? QString() : filenames[0]);
239 connect(tabManager, SIGNAL(tabCountChanged(int)), this, SLOT(setTabToolBarVisible(int)));
240 this->setTabToolBarVisible(tabManager->count());
241 tabToolBarContents->layout()->addWidget(tabManager->getTabHeader());
242 editorDockContents->layout()->addWidget(tabManager->getTabContent());
243
244 connect(Preferences::inst(), SIGNAL(consoleFontChanged(const QString&, uint)), this->console, SLOT(setFont(const QString&, uint)));
245
246 const QString version = QString("<b>OpenSCAD %1</b>").arg(QString::fromStdString(openscad_versionnumber));
247 const QString weblink = "<a href=\"https://www.openscad.org/\">https://www.openscad.org/</a><br>";
248 this->console->setFont(
249 Preferences::inst()->getValue("advanced/consoleFontFamily").toString(),
250 Preferences::inst()->getValue("advanced/consoleFontSize").toUInt()
251 );
252
253 consoleOutputRaw(version);
254 consoleOutputRaw(weblink);
255 consoleOutputRaw(copyrighttext);
256 this->consoleUpdater->start(0); // Show "Loaded Design" message from TabManager
257
258 connect(this->errorLogWidget,SIGNAL(openFile(QString,int)),this,SLOT(openFileFromPath(QString,int)));
259 connect(this->console,SIGNAL(openFile(QString,int)),this,SLOT(openFileFromPath(QString,int)));
260
261 connect(Preferences::inst()->ButtonConfig, SIGNAL(inputMappingChanged()), InputDriverManager::instance(), SLOT(onInputMappingUpdated()), Qt::UniqueConnection);
262 connect(Preferences::inst()->AxisConfig, SIGNAL(inputMappingChanged()), InputDriverManager::instance(), SLOT(onInputMappingUpdated()), Qt::UniqueConnection);
263 connect(Preferences::inst()->AxisConfig, SIGNAL(inputCalibrationChanged()), InputDriverManager::instance(), SLOT(onInputCalibrationUpdated()), Qt::UniqueConnection);
264 connect(Preferences::inst()->AxisConfig, SIGNAL(inputGainChanged()), InputDriverManager::instance(), SLOT(onInputGainUpdated()), Qt::UniqueConnection);
265
266 setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea);
267 setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea);
268 setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
269 setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
270
271 this->setAttribute(Qt::WA_DeleteOnClose);
272
273 scadApp->windowManager.add(this);
274
275 #ifdef ENABLE_CGAL
276 this->cgalworker = new CGALWorker();
277 connect(this->cgalworker, SIGNAL(done(shared_ptr<const Geometry>)),
278 this, SLOT(actionRenderDone(shared_ptr<const Geometry>)));
279 #endif
280
281 #ifdef ENABLE_CGAL
282 this->cgalRenderer = nullptr;
283 #endif
284 #ifdef ENABLE_OPENCSG
285 this->opencsgRenderer = nullptr;
286 #endif
287 this->thrownTogetherRenderer = nullptr;
288
289 root_node = nullptr;
290
291 this->anim_step = 0;
292 this->anim_numsteps = 0;
293 this->anim_tval = 0.0;
294 this->anim_dumping = false;
295 this->anim_dump_start_step = 0;
296
297 this->qglview->statusLabel = new QLabel(this);
298 this->qglview->statusLabel->setMinimumWidth(100);
299 statusBar()->addWidget(this->qglview->statusLabel);
300
301 QSettingsCached settings;
302 auto s = Settings::Settings::inst();
303 this->qglview->setMouseCentricZoom(s->get(Settings::Settings::mouseCentricZoom).toBool());
304
305 animate_timer = new QTimer(this);
306 connect(animate_timer, SIGNAL(timeout()), this, SLOT(updateTVal()));
307
308 autoReloadTimer = new QTimer(this);
309 autoReloadTimer->setSingleShot(false);
310 autoReloadTimer->setInterval(autoReloadPollingPeriodMS);
311 connect(autoReloadTimer, SIGNAL(timeout()), this, SLOT(checkAutoReload()));
312
313 waitAfterReloadTimer = new QTimer(this);
314 waitAfterReloadTimer->setSingleShot(true);
315 waitAfterReloadTimer->setInterval(autoReloadPollingPeriodMS);
316 connect(waitAfterReloadTimer, SIGNAL(timeout()), this, SLOT(waitAfterReload()));
317 connect(this->parameterWidget, SIGNAL(previewRequested(bool)), this, SLOT(actionRenderPreview(bool)));
318 connect(Preferences::inst(), SIGNAL(ExperimentalChanged()), this, SLOT(changeParameterWidget()));
319 connect(this->e_tval, SIGNAL(textChanged(QString)), this, SLOT(updatedAnimTval()));
320 connect(this->e_fps, SIGNAL(textChanged(QString)), this, SLOT(updatedAnimFps()));
321 connect(this->e_fsteps, SIGNAL(textChanged(QString)), this, SLOT(updatedAnimSteps()));
322 connect(this->e_dump, SIGNAL(toggled(bool)), this, SLOT(updatedAnimDump(bool)));
323
324 progressThrottle->start();
325
326 animate_panel->hide();
327 this->hideFind();
328 frameCompileResult->hide();
329 this->labelCompileResultMessage->setOpenExternalLinks(false);
330 connect(this->labelCompileResultMessage, SIGNAL(linkActivated(QString)), SLOT(showLink(QString)));
331
332 // File menu
333 connect(this->fileActionNewWindow, SIGNAL(triggered()), this, SLOT(actionNewWindow()));
334 connect(this->fileActionNew, SIGNAL(triggered()), tabManager, SLOT(actionNew()));
335 connect(this->fileActionOpenWindow, SIGNAL(triggered()), this, SLOT(actionOpenWindow()));
336 connect(this->fileActionOpen, SIGNAL(triggered()), this, SLOT(actionOpen()));
337 connect(this->fileActionSave, SIGNAL(triggered()), this, SLOT(actionSave()));
338 connect(this->fileActionSaveAs, SIGNAL(triggered()), this, SLOT(actionSaveAs()));
339 connect(this->fileActionSaveAll, SIGNAL(triggered()), tabManager, SLOT(saveAll()));
340 connect(this->fileActionReload, SIGNAL(triggered()), this, SLOT(actionReload()));
341 connect(this->fileActionClose, SIGNAL(triggered()), tabManager, SLOT(closeCurrentTab()));
342 connect(this->fileActionQuit, SIGNAL(triggered()), this, SLOT(quit()));
343 connect(this->fileShowLibraryFolder, SIGNAL(triggered()), this, SLOT(actionShowLibraryFolder()));
344 #ifndef __APPLE__
345 auto shortcuts = this->fileActionSave->shortcuts();
346 this->fileActionSave->setShortcuts(shortcuts);
347 shortcuts = this->fileActionReload->shortcuts();
348 shortcuts.push_back(QKeySequence(Qt::Key_F3));
349 this->fileActionReload->setShortcuts(shortcuts);
350 #endif
351
352 this->menuOpenRecent->addSeparator();
353 this->menuOpenRecent->addAction(this->fileActionClearRecent);
354 connect(this->fileActionClearRecent, SIGNAL(triggered()),
355 this, SLOT(clearRecentFiles()));
356
357 show_examples();
358
359 connect(this->editActionNextTab, SIGNAL(triggered()), tabManager, SLOT(nextTab()));
360 connect(this->editActionPrevTab, SIGNAL(triggered()), tabManager, SLOT(prevTab()));
361
362 connect(this->editActionCopyViewport, SIGNAL(triggered()), this, SLOT(actionCopyViewport()));
363 connect(this->editActionConvertTabsToSpaces, SIGNAL(triggered()), this, SLOT(convertTabsToSpaces()));
364 connect(this->editActionCopyVPT, SIGNAL(triggered()), this, SLOT(copyViewportTranslation()));
365 connect(this->editActionCopyVPR, SIGNAL(triggered()), this, SLOT(copyViewportRotation()));
366 connect(this->editActionCopyVPD, SIGNAL(triggered()), this, SLOT(copyViewportDistance()));
367 connect(this->editActionCopyVPF, SIGNAL(triggered()), this, SLOT(copyViewportFov()));
368 connect(this->editActionPreferences, SIGNAL(triggered()), this, SLOT(preferences()));
369 // Edit->Find
370 connect(this->editActionFind, SIGNAL(triggered()), this, SLOT(showFind()));
371 connect(this->editActionFindAndReplace, SIGNAL(triggered()), this, SLOT(showFindAndReplace()));
372 #ifdef Q_OS_WIN
373 this->editActionFindAndReplace->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_F));
374 #endif
375 connect(this->editActionFindNext, SIGNAL(triggered()), this, SLOT(findNext()));
376 connect(this->editActionFindPrevious, SIGNAL(triggered()), this, SLOT(findPrev()));
377 connect(this->editActionUseSelectionForFind, SIGNAL(triggered()), this, SLOT(useSelectionForFind()));
378
379 // Design menu
380 connect(this->designActionAutoReload, SIGNAL(toggled(bool)), this, SLOT(autoReloadSet(bool)));
381 connect(this->designActionReloadAndPreview, SIGNAL(triggered()), this, SLOT(actionReloadRenderPreview()));
382 connect(this->designActionPreview, SIGNAL(triggered()), this, SLOT(actionRenderPreview()));
383 #ifdef ENABLE_CGAL
384 connect(this->designActionRender, SIGNAL(triggered()), this, SLOT(actionRender()));
385 #else
386 this->designActionRender->setVisible(false);
387 #endif
388 connect(this->designAction3DPrint, SIGNAL(triggered()), this, SLOT(action3DPrint()));
389 connect(this->designCheckValidity, SIGNAL(triggered()), this, SLOT(actionCheckValidity()));
390 connect(this->designActionDisplayAST, SIGNAL(triggered()), this, SLOT(actionDisplayAST()));
391 connect(this->designActionDisplayCSGTree, SIGNAL(triggered()), this, SLOT(actionDisplayCSGTree()));
392 connect(this->designActionDisplayCSGProducts, SIGNAL(triggered()), this, SLOT(actionDisplayCSGProducts()));
393 connect(this->fileActionExportSTL, SIGNAL(triggered()), this, SLOT(actionExportSTL()));
394 connect(this->fileActionExport3MF, SIGNAL(triggered()), this, SLOT(actionExport3MF()));
395 connect(this->fileActionExportOFF, SIGNAL(triggered()), this, SLOT(actionExportOFF()));
396 connect(this->fileActionExportAMF, SIGNAL(triggered()), this, SLOT(actionExportAMF()));
397 connect(this->fileActionExportDXF, SIGNAL(triggered()), this, SLOT(actionExportDXF()));
398 connect(this->fileActionExportSVG, SIGNAL(triggered()), this, SLOT(actionExportSVG()));
399 connect(this->fileActionExportPDF, SIGNAL(triggered()), this, SLOT(actionExportPDF()));
400 connect(this->fileActionExportCSG, SIGNAL(triggered()), this, SLOT(actionExportCSG()));
401 connect(this->fileActionExportImage, SIGNAL(triggered()), this, SLOT(actionExportImage()));
402 connect(this->designActionFlushCaches, SIGNAL(triggered()), this, SLOT(actionFlushCaches()));
403
404 #ifndef ENABLE_LIB3MF
405 this->fileActionExport3MF->setVisible(false);
406 #endif
407
408 #ifndef ENABLE_3D_PRINTING
409 this->designAction3DPrint->setVisible(false);
410 this->designAction3DPrint->setEnabled(false);
411 #endif
412
413 // View menu
414 #ifndef ENABLE_OPENCSG
415 this->viewActionPreview->setVisible(false);
416 #else
417 connect(this->viewActionPreview, SIGNAL(triggered()), this, SLOT(viewModePreview()));
418 if (!this->qglview->hasOpenCSGSupport()) {
419 this->viewActionPreview->setEnabled(false);
420 }
421 #endif
422
423 #ifdef ENABLE_CGAL
424 connect(this->viewActionSurfaces, SIGNAL(triggered()), this, SLOT(viewModeSurface()));
425 connect(this->viewActionWireframe, SIGNAL(triggered()), this, SLOT(viewModeWireframe()));
426 #else
427 this->viewActionSurfaces->setVisible(false);
428 this->viewActionWireframe->setVisible(false);
429 #endif
430 connect(this->viewActionThrownTogether, SIGNAL(triggered()), this, SLOT(viewModeThrownTogether()));
431 connect(this->viewActionShowEdges, SIGNAL(triggered()), this, SLOT(viewModeShowEdges()));
432 connect(this->viewActionShowAxes, SIGNAL(triggered()), this, SLOT(viewModeShowAxes()));
433 connect(this->viewActionShowCrosshairs, SIGNAL(triggered()), this, SLOT(viewModeShowCrosshairs()));
434 connect(this->viewActionShowScaleProportional, SIGNAL(triggered()), this, SLOT(viewModeShowScaleProportional()));
435 connect(this->viewActionAnimate, SIGNAL(triggered()), this, SLOT(viewModeAnimate()));
436 connect(this->viewActionTop, SIGNAL(triggered()), this, SLOT(viewAngleTop()));
437 connect(this->viewActionBottom, SIGNAL(triggered()), this, SLOT(viewAngleBottom()));
438 connect(this->viewActionLeft, SIGNAL(triggered()), this, SLOT(viewAngleLeft()));
439 connect(this->viewActionRight, SIGNAL(triggered()), this, SLOT(viewAngleRight()));
440 connect(this->viewActionFront, SIGNAL(triggered()), this, SLOT(viewAngleFront()));
441 connect(this->viewActionBack, SIGNAL(triggered()), this, SLOT(viewAngleBack()));
442 connect(this->viewActionDiagonal, SIGNAL(triggered()), this, SLOT(viewAngleDiagonal()));
443 connect(this->viewActionCenter, SIGNAL(triggered()), this, SLOT(viewCenter()));
444 connect(this->viewActionResetView, SIGNAL(triggered()), this, SLOT(viewResetView()));
445 connect(this->viewActionViewAll, SIGNAL(triggered()), this, SLOT(viewAll()));
446 connect(this->viewActionPerspective, SIGNAL(triggered()), this, SLOT(viewPerspective()));
447 connect(this->viewActionOrthogonal, SIGNAL(triggered()), this, SLOT(viewOrthogonal()));
448 connect(this->viewActionZoomIn, SIGNAL(triggered()), qglview, SLOT(ZoomIn()));
449 connect(this->viewActionZoomOut, SIGNAL(triggered()), qglview, SLOT(ZoomOut()));
450 connect(this->viewActionHideEditorToolBar, SIGNAL(triggered()), this, SLOT(hideEditorToolbar()));
451 connect(this->viewActionHide3DViewToolBar, SIGNAL(triggered()), this, SLOT(hide3DViewToolbar()));
452 connect(this->windowActionHideEditor, SIGNAL(triggered()), this, SLOT(hideEditor()));
453 connect(this->windowActionHideConsole, SIGNAL(triggered()), this, SLOT(hideConsole()));
454 connect(this->windowActionHideCustomizer, SIGNAL(triggered()), this, SLOT(hideParameters()));
455 connect(this->windowActionHideErrorLog, SIGNAL(triggered()), this, SLOT(hideErrorLog()));
456 // Help menu
457 connect(this->helpActionAbout, SIGNAL(triggered()), this, SLOT(helpAbout()));
458 connect(this->helpActionHomepage, SIGNAL(triggered()), this, SLOT(helpHomepage()));
459 connect(this->helpActionManual, SIGNAL(triggered()), this, SLOT(helpManual()));
460 connect(this->helpActionCheatSheet, SIGNAL(triggered()), this, SLOT(helpCheatSheet()));
461 connect(this->helpActionLibraryInfo, SIGNAL(triggered()), this, SLOT(helpLibrary()));
462 connect(this->helpActionFontInfo, SIGNAL(triggered()), this, SLOT(helpFontInfo()));
463
464 #ifdef OPENSCAD_UPDATER
465 this->menuBar()->addMenu(AutoUpdater::updater()->updateMenu);
466 #endif
467
468 connect(this->qglview, SIGNAL(doAnimateUpdate()), this, SLOT(animateUpdate()));
469 connect(this->qglview, SIGNAL(doSelectObject(QPoint)), this, SLOT(selectObject(QPoint)));
470
471 connect(Preferences::inst(), SIGNAL(requestRedraw()), this->qglview, SLOT(updateGL()));
472 connect(Preferences::inst(), SIGNAL(updateMouseCentricZoom(bool)), this->qglview, SLOT(setMouseCentricZoom(bool)));
473 connect(Preferences::inst(), SIGNAL(updateReorderMode(bool)), this, SLOT(updateReorderMode(bool)));
474 connect(Preferences::inst(), SIGNAL(updateUndockMode(bool)), this, SLOT(updateUndockMode(bool)));
475 connect(Preferences::inst(), SIGNAL(openCSGSettingsChanged()),
476 this, SLOT(openCSGSettingsChanged()));
477 connect(Preferences::inst(), SIGNAL(colorSchemeChanged(const QString&)),
478 this, SLOT(setColorScheme(const QString&)));
479
480 Preferences::inst()->apply_win(); // not sure if to be commented, checked must not be commented(done some changes in apply())
481
482 QString cs = Preferences::inst()->getValue("3dview/colorscheme").toString();
483 this->setColorScheme(cs);
484
485 //find and replace panel
486 connect(this->findTypeComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(selectFindType(int)));
487 connect(this->findInputField, SIGNAL(textChanged(QString)), this, SLOT(findString(QString)));
488 connect(this->findInputField, SIGNAL(returnPressed()), this->findNextButton, SLOT(animateClick()));
489 find_panel->installEventFilter(this);
490 if (QApplication::clipboard()->supportsFindBuffer()) {
491 connect(this->findInputField, SIGNAL(textChanged(QString)), this, SLOT(updateFindBuffer(QString)));
492 connect(QApplication::clipboard(), SIGNAL(findBufferChanged()), this, SLOT(findBufferChanged()));
493 // With Qt 4.8.6, there seems to be a bug that often gives an incorrect findbuffer content when
494 // the app receives focus for the first time
495 this->findInputField->setText(QApplication::clipboard()->text(QClipboard::FindBuffer));
496 }
497
498 connect(this->findPrevButton, SIGNAL(clicked()), this, SLOT(findPrev()));
499 connect(this->findNextButton, SIGNAL(clicked()), this, SLOT(findNext()));
500 connect(this->cancelButton, SIGNAL(clicked()), this, SLOT(hideFind()));
501 connect(this->replaceButton, SIGNAL(clicked()), this, SLOT(replace()));
502 connect(this->replaceAllButton, SIGNAL(clicked()), this, SLOT(replaceAll()));
503 connect(this->replaceInputField, SIGNAL(returnPressed()), this->replaceButton, SLOT(animateClick()));
504 addKeyboardShortCut(this->viewerToolBar->actions());
505 addKeyboardShortCut(this->editortoolbar->actions());
506
507 InputDriverManager::instance()->registerActions(this->menuBar()->actions(),"");
508 Preferences* instance = Preferences::inst();
509 instance->ButtonConfig->init();
510
511 initActionIcon(fileActionNew, ":/images/blackNew.png", ":/images/Document-New-128.png");
512 initActionIcon(fileActionOpen, ":/images/Open-32.png", ":/images/Open-128.png");
513 initActionIcon(fileActionSave, ":/images/Save-32.png", ":/images/Save-128.png");
514 initActionIcon(editActionZoomTextIn, ":/images/zoom-text-in.png", ":/images/zoom-text-in-white.png");
515 initActionIcon(editActionZoomTextOut, ":/images/zoom-text-out.png", ":/images/zoom-text-out-white.png");
516 initActionIcon(designActionRender, ":/images/render-32.png", ":/images/render-32-white.png");
517 initActionIcon(designAction3DPrint, ":/images/3dprint-32.png", ":/images/3dprint-32-white.png");
518 initActionIcon(viewActionShowAxes, ":/images/blackaxes.png", ":/images/axes.png");
519 initActionIcon(viewActionShowEdges, ":/images/Rotation-32.png", ":/images/grid.png");
520 initActionIcon(viewActionZoomIn, ":/images/zoomin.png", ":/images/Zoom-In-32.png");
521 initActionIcon(viewActionZoomOut, ":/images/zoomout.png", ":/images/Zoom-Out-32.png");
522 initActionIcon(viewActionTop, ":/images/blackUp.png", ":/images/up.png");
523 initActionIcon(viewActionBottom, ":/images/blackbottom.png", ":/images/bottom.png");
524 initActionIcon(viewActionLeft, ":/images/blackleft.png", ":/images/left.png");
525 initActionIcon(viewActionRight, ":/images/rightright.png", ":/images/right.png");
526 initActionIcon(viewActionFront, ":/images/blackfront.png", ":/images/front.png");
527 initActionIcon(viewActionBack, ":/images/blackback.png", ":/images/back.png");
528 initActionIcon(viewActionSurfaces, ":/images/surface.png", ":/images/surfaceWhite.png");
529 initActionIcon(viewActionWireframe, ":/images/wireframe1.png", ":/images/wireframeWhite.png");
530 initActionIcon(viewActionShowCrosshairs, ":/images/cross.png", ":/images/crosswhite.png");
531 initActionIcon(viewActionPerspective, ":/images/perspective1.png", ":/images/perspective1white.png");
532 initActionIcon(viewActionOrthogonal, ":/images/orthogonal.png", ":/images/orthogonalwhite.png");
533 initActionIcon(designActionPreview, ":/images/preview-32.png", ":/images/preview-32-white.png");
534 initActionIcon(viewActionAnimate, ":/images/animate.png", ":/images/animate.png");
535 initActionIcon(fileActionExportSTL, ":/images/STL.png", ":/images/STL-white.png");
536 initActionIcon(fileActionExportAMF, ":/images/AMF.png", ":/images/AMF-white.png");
537 initActionIcon(fileActionExport3MF, ":/images/3MF.png", ":/images/3MF-white.png");
538 initActionIcon(fileActionExportOFF, ":/images/OFF.png", ":/images/OFF-white.png");
539 initActionIcon(fileActionExportDXF, ":/images/DXF.png", ":/images/DXF-white.png");
540 initActionIcon(fileActionExportSVG, ":/images/SVG.png", ":/images/SVG-white.png");
541 initActionIcon(fileActionExportCSG, ":/images/CSG.png", ":/images/CSG-white.png");
542 initActionIcon(fileActionExportImage, ":/images/PNG.png", ":/images/PNG-white.png");
543 initActionIcon(viewActionViewAll, ":/images/zoom-all.png", ":/images/zoom-all-white.png");
544 initActionIcon(editActionUndo, ":/images/Command-Undo-32.png", ":/images/Command-Undo-32-white.png");
545 initActionIcon(editActionRedo, ":/images/Command-Redo-32.png", ":/images/Command-Redo-32-white.png");
546 initActionIcon(editActionUnindent, ":/images/Decrease-Indent-32.png", ":/images/Decrease-Indent-32-white.png");
547 initActionIcon(editActionIndent, ":/images/Increase-Indent-32.png", ":/images/Increase-Indent-32-white.png");
548 initActionIcon(viewActionResetView, ":/images/Command-Reset-32.png", ":/images/Command-Reset-32-white.png");
549 initActionIcon(viewActionShowScaleProportional, ":/images/scalemarkers.png", ":/images/scalemarkers-white.png");
550
551 // fetch window states to be restored after restoreState() call
552 bool hideConsole = settings.value("view/hideConsole").toBool();
553 bool hideEditor = settings.value("view/hideEditor").toBool();
554 bool hideCustomizer = settings.value("view/hideCustomizer").toBool();
555 bool hideErrorLog = settings.value("view/hideErrorLog").toBool();
556 bool hideEditorToolbar = settings.value("view/hideEditorToolbar").toBool();
557 bool hide3DViewToolbar = settings.value("view/hide3DViewToolbar").toBool();
558
559 // make sure it looks nice..
560 auto windowState = settings.value("window/state", QByteArray()).toByteArray();
561 restoreState(windowState);
562 resize(settings.value("window/size", QSize(800, 600)).toSize());
563 move(settings.value("window/position", QPoint(0, 0)).toPoint());
564 updateWindowSettings(hideConsole, hideEditor, hideCustomizer, hideErrorLog, hideEditorToolbar, hide3DViewToolbar);
565
566 if (windowState.size() == 0) {
567 /*
568 * This triggers only in case the configuration file has no
569 * window state information (or no configuration file at all).
570 * When this happens, the editor would default to a very ugly
571 * width due to the dock widget layout. This overwrites the
572 * value reported via sizeHint() to a width a bit smaller than
573 * half the main window size (either the one loaded from the
574 * configuration or the default value of 800).
575 * The height is only a dummy value which will be essentially
576 * ignored by the layouting as the editor is set to expand to
577 * fill the available space.
578 */
579 activeEditor->setInitialSizeHint(QSize((5 * this->width() / 11), 100));
580 } else {
581 #ifdef Q_OS_WIN
582 // Try moving the main window into the display range, this
583 // can occur when closing OpenSCAD on a second monitor which
584 // is not available at the time the application is started
585 // again.
586 // On Windows that causes the main window to open in a not
587 // easily reachable place.
588 auto desktop = QApplication::desktop();
589 auto desktopRect = desktop->frameGeometry().adjusted(250, 150, -250, -150).normalized();
590 auto windowRect = frameGeometry();
591 if (!desktopRect.intersects(windowRect)) {
592 windowRect.moveCenter(desktopRect.center());
593 windowRect = windowRect.intersected(desktopRect);
594 move(windowRect.topLeft());
595 resize(windowRect.size());
596 }
597 #endif
598 }
599
600 connect(this->editorDock, SIGNAL(topLevelChanged(bool)), this, SLOT(editorTopLevelChanged(bool)));
601 connect(this->consoleDock, SIGNAL(topLevelChanged(bool)), this, SLOT(consoleTopLevelChanged(bool)));
602 connect(this->parameterDock, SIGNAL(topLevelChanged(bool)), this, SLOT(parameterTopLevelChanged(bool)));
603 connect(this->errorLogDock, SIGNAL(topLevelChanged(bool)), this, SLOT(errorLogTopLevelChanged(bool)));
604
605 // display this window and check for OpenGL 2.0 (OpenCSG) support
606 viewModeThrownTogether();
607 show();
608
609 setCurrentOutput();
610
611 #ifdef ENABLE_OPENCSG
612 viewModePreview();
613 #else
614 viewModeThrownTogether();
615 #endif
616 loadViewSettings();
617 loadDesignSettings();
618
619 setAcceptDrops(true);
620 clearCurrentOutput();
621
622 for(int i = 1; i < filenames.size(); ++i)
623 tabManager->createTab(filenames[i]);
624
625 //handle the hide/show of exportSTL action in view toolbar according to the visibility of editor dock
626 if (!editorDock->isVisible()) {
627 QAction *beforeAction = viewerToolBar->actions().at(2); //a separator, not a part of the class
628 viewerToolBar->insertAction(beforeAction, this->fileActionExportSTL);
629 }
630
631 this->selector = std::unique_ptr<MouseSelector>(new MouseSelector(this->qglview));
632 activeEditor->setFocus();
633 }
634
openFileFromPath(QString path,int line)635 void MainWindow::openFileFromPath(QString path,int line)
636 {
637 if (editorDock->isVisible()) {
638 activeEditor->setFocus();
639 if(!path.isEmpty()) tabManager->open(path);
640 activeEditor->setFocus();
641 activeEditor->setCursorPosition(line,0);
642 }
643 }
644
initActionIcon(QAction * action,const char * darkResource,const char * lightResource)645 void MainWindow::initActionIcon(QAction *action, const char *darkResource, const char *lightResource)
646 {
647 int defaultcolor = viewerToolBar->palette().background().color().lightness();
648 const char *resource = (defaultcolor > 165) ? darkResource : lightResource;
649 action->setIcon(QIcon(resource));
650 }
651
addKeyboardShortCut(const QList<QAction * > & actions)652 void MainWindow::addKeyboardShortCut(const QList<QAction *> &actions)
653 {
654 for (auto &action : actions) {
655 // prevent adding shortcut twice if action is added to multiple toolbars
656 if (action->toolTip().contains(" ")) {
657 continue;
658 }
659
660 const QString shortCut(action->shortcut().toString(QKeySequence::NativeText));
661 if (shortCut.isEmpty()) {
662 continue;
663 }
664
665 const QString toolTip("%1 <span style=\"color: gray; font-size: small; font-style: italic\">%2</span>");
666 action->setToolTip(toolTip.arg(action->toolTip(), shortCut));
667 }
668 }
669
670 /**
671 * Update window settings that get overwritten by the restoreState()
672 * Qt call. So the values are loaded before the call and restored here
673 * regardless of the (potential outdated) serialized state.
674 */
updateWindowSettings(bool console,bool editor,bool customizer,bool errorLog,bool editorToolbar,bool viewToolbar)675 void MainWindow::updateWindowSettings(bool console, bool editor, bool customizer, bool errorLog, bool editorToolbar, bool viewToolbar)
676 {
677 windowActionHideEditor->setChecked(editor);
678 hideEditor();
679 windowActionHideConsole->setChecked(console);
680 hideConsole();
681 windowActionHideErrorLog->setChecked(errorLog);
682 hideErrorLog();
683 windowActionHideCustomizer->setChecked(customizer);
684 hideParameters();
685
686 viewActionHideEditorToolBar->setChecked(editorToolbar);
687 hideEditorToolbar();
688 viewActionHide3DViewToolBar->setChecked(viewToolbar);
689 hide3DViewToolbar();
690 }
691
onAxisChanged(InputEventAxisChanged *)692 void MainWindow::onAxisChanged(InputEventAxisChanged *)
693 {
694
695 }
696
onButtonChanged(InputEventButtonChanged *)697 void MainWindow::onButtonChanged(InputEventButtonChanged *)
698 {
699
700 }
701
onTranslateEvent(InputEventTranslate * event)702 void MainWindow::onTranslateEvent(InputEventTranslate *event)
703 {
704 double zoomFactor = 0.001 * qglview->cam.zoomValue();
705
706 if(event->viewPortRelative){
707 qglview->translate(event->x, event->y, event->z, event->relative, true);
708 }else{
709 qglview->translate(zoomFactor * event->x, event->y, zoomFactor * event->z, event->relative, false);
710 }
711
712 }
713
onRotateEvent(InputEventRotate * event)714 void MainWindow::onRotateEvent(InputEventRotate *event)
715 {
716 qglview->rotate(event->x, event->y, event->z, event->relative);
717 }
718
onRotate2Event(InputEventRotate2 * event)719 void MainWindow::onRotate2Event(InputEventRotate2 *event)
720 {
721 qglview->rotate2(event->x, event->y, event->z);
722 }
723
onActionEvent(InputEventAction * event)724 void MainWindow::onActionEvent(InputEventAction *event)
725 {
726 QAction *action = findAction(this->menuBar()->actions(), event->action);
727 if (action) {
728 action->trigger();
729 }else if("viewActionTogglePerspective" == event->action){
730 viewTogglePerspective();
731 }
732 }
733
onZoomEvent(InputEventZoom * event)734 void MainWindow::onZoomEvent(InputEventZoom *event)
735 {
736 qglview->zoom(event->zoom, event->relative);
737 }
738
loadViewSettings()739 void MainWindow::loadViewSettings(){
740 QSettingsCached settings;
741
742 if (settings.value("view/showEdges").toBool()) {
743 viewActionShowEdges->setChecked(true);
744 viewModeShowEdges();
745 }
746 if (settings.value("view/showAxes", true).toBool()) {
747 viewActionShowAxes->setChecked(true);
748 viewModeShowAxes();
749 }
750 if (settings.value("view/showCrosshairs").toBool()) {
751 viewActionShowCrosshairs->setChecked(true);
752 viewModeShowCrosshairs();
753 }
754 if (settings.value("view/showScaleProportional", true).toBool()) {
755 viewActionShowScaleProportional->setChecked(true);
756 viewModeShowScaleProportional();
757 }
758 if (settings.value("view/orthogonalProjection").toBool()) {
759 viewOrthogonal();
760 } else {
761 viewPerspective();
762 }
763
764 updateUndockMode(Preferences::inst()->getValue("advanced/undockableWindows").toBool());
765 updateReorderMode(Preferences::inst()->getValue("advanced/reorderWindows").toBool());
766 }
767
loadDesignSettings()768 void MainWindow::loadDesignSettings()
769 {
770 QSettingsCached settings;
771 if (settings.value("design/autoReload", true).toBool()) {
772 designActionAutoReload->setChecked(true);
773 }
774 auto polySetCacheSizeMB = Preferences::inst()->getValue("advanced/polysetCacheSizeMB").toUInt();
775 GeometryCache::instance()->setMaxSizeMB(polySetCacheSizeMB);
776 #ifdef ENABLE_CGAL
777 auto cgalCacheSizeMB = Preferences::inst()->getValue("advanced/cgalCacheSizeMB").toUInt();
778 CGALCache::instance()->setMaxSizeMB(cgalCacheSizeMB);
779 #endif
780 }
781
updateUndockMode(bool undockMode)782 void MainWindow::updateUndockMode(bool undockMode)
783 {
784 MainWindow::undockMode = undockMode;
785 if (undockMode) {
786 editorDock->setFeatures(editorDock->features() | QDockWidget::DockWidgetFloatable);
787 consoleDock->setFeatures(consoleDock->features() | QDockWidget::DockWidgetFloatable);
788 parameterDock->setFeatures(parameterDock->features() | QDockWidget::DockWidgetFloatable);
789 errorLogDock->setFeatures(errorLogDock->features() | QDockWidget::DockWidgetFloatable);
790 } else {
791 if (editorDock->isFloating()) {
792 editorDock->setFloating(false);
793 }
794 editorDock->setFeatures(editorDock->features() & ~QDockWidget::DockWidgetFloatable);
795 if (consoleDock->isFloating()) {
796 consoleDock->setFloating(false);
797 }
798 consoleDock->setFeatures(consoleDock->features() & ~QDockWidget::DockWidgetFloatable);
799 if (parameterDock->isFloating()) {
800 parameterDock->setFloating(false);
801 }
802 parameterDock->setFeatures(parameterDock->features() & ~QDockWidget::DockWidgetFloatable);
803 if (errorLogDock->isFloating()) {
804 errorLogDock->setFloating(false);
805 }
806 errorLogDock->setFeatures(errorLogDock->features() & ~QDockWidget::DockWidgetFloatable);
807 }
808 }
809
updateReorderMode(bool reorderMode)810 void MainWindow::updateReorderMode(bool reorderMode)
811 {
812 MainWindow::reorderMode = reorderMode;
813 editorDock->setTitleBarWidget(reorderMode ? nullptr : editorDockTitleWidget);
814 consoleDock->setTitleBarWidget(reorderMode ? nullptr : consoleDockTitleWidget);
815 parameterDock->setTitleBarWidget(reorderMode ? nullptr : parameterDockTitleWidget);
816 errorLogDock->setTitleBarWidget(reorderMode ? nullptr : errorLogDockTitleWidget);
817 }
818
~MainWindow()819 MainWindow::~MainWindow()
820 {
821 // If root_module is not null then it will be the same as parsed_module,
822 // so no need to delete it.
823 delete parsed_module;
824 delete root_node;
825 #ifdef ENABLE_CGAL
826 this->root_geom.reset();
827 delete this->cgalRenderer;
828 #endif
829 #ifdef ENABLE_OPENCSG
830 delete this->opencsgRenderer;
831 #endif
832 delete this->thrownTogetherRenderer;
833 scadApp->windowManager.remove(this);
834 if (scadApp->windowManager.getWindows().size() == 0) {
835 // Quit application even in case some other windows like
836 // Preferences are still open.
837 this->quit();
838 }
839 }
840
showProgress()841 void MainWindow::showProgress()
842 {
843 updateStatusBar(qobject_cast<ProgressWidget*>(sender()));
844 }
845
report_func(const class AbstractNode *,void * vp,int mark)846 void MainWindow::report_func(const class AbstractNode*, void *vp, int mark)
847 {
848 // limit to progress bar update calls to 5 per second
849 static const qint64 MIN_TIMEOUT = 200;
850 if (progressThrottle->hasExpired(MIN_TIMEOUT)) {
851 progressThrottle->start();
852
853 auto thisp = static_cast<MainWindow*>(vp);
854 auto v = static_cast<int>((mark*1000.0) / progress_report_count);
855 auto permille = v < 1000 ? v : 999;
856 if (permille > thisp->progresswidget->value()) {
857 QMetaObject::invokeMethod(thisp->progresswidget, "setValue", Qt::QueuedConnection,
858 Q_ARG(int, permille));
859 QApplication::processEvents();
860 }
861
862 // FIXME: Check if cancel was requested by e.g. Application quit
863 if (thisp->progresswidget->wasCanceled()) throw ProgressCancelException();
864 }
865 }
866
network_progress_func(const double permille)867 bool MainWindow::network_progress_func(const double permille)
868 {
869 QMetaObject::invokeMethod(this->progresswidget, "setValue", Qt::QueuedConnection, Q_ARG(int, (int)permille));
870 return (progresswidget && progresswidget->wasCanceled());
871 }
872
updateRecentFiles(EditorInterface * edt)873 void MainWindow::updateRecentFiles(EditorInterface *edt)
874 {
875 // Check that the canonical file path exists - only update recent files
876 // if it does. Should prevent empty list items on initial open etc.
877 QFileInfo fileinfo(edt->filepath);
878 auto infoFileName = fileinfo.absoluteFilePath();
879 QSettingsCached settings; // already set up properly via main.cpp
880 auto files = settings.value("recentFileList").toStringList();
881 files.removeAll(infoFileName);
882 files.prepend(infoFileName);
883 while (files.size() > UIUtils::maxRecentFiles) files.removeLast();
884 settings.setValue("recentFileList", files);
885
886 for (auto &widget : QApplication::topLevelWidgets()) {
887 auto mainWin = qobject_cast<MainWindow *>(widget);
888 if (mainWin) {
889 mainWin->updateRecentFileActions();
890 }
891 }
892 }
893
setTabToolBarVisible(int count)894 void MainWindow::setTabToolBarVisible(int count)
895 {
896 tabCount = count;
897 tabToolBar->setVisible((tabCount > 1) && editorDock->isVisible());
898 }
899
updatedAnimTval()900 void MainWindow::updatedAnimTval()
901 {
902 bool t_ok;
903 double t = this->e_tval->text().toDouble(&t_ok);
904 // Clamp t to 0-1
905 if (t_ok) {
906 this->anim_tval = t < 0 ? 0.0 : ((t > 1.0) ? 1.0 : t);
907 }
908 else {
909 this->anim_tval = 0.0;
910 }
911 emit actionRenderPreview(true);
912 }
913
updatedAnimFps()914 void MainWindow::updatedAnimFps()
915 {
916 bool fps_ok;
917 double fps = this->e_fps->text().toDouble(&fps_ok);
918 animate_timer->stop();
919 if (fps_ok && fps > 0 && this->anim_numsteps > 0) {
920 this->anim_step = int(this->anim_tval * this->anim_numsteps) % this->anim_numsteps;
921 animate_timer->setSingleShot(false);
922 animate_timer->setInterval(int(1000 / fps));
923 animate_timer->start();
924 }
925 }
926
updatedAnimSteps()927 void MainWindow::updatedAnimSteps()
928 {
929 bool steps_ok;
930 int numsteps = this->e_fsteps->text().toInt(&steps_ok);
931 if (steps_ok) {
932 this->anim_numsteps = numsteps;
933 updatedAnimFps(); // Make sure we start
934 }
935 else {
936 this->anim_numsteps = 0;
937 }
938 anim_dumping=false;
939 }
940
updatedAnimDump(bool checked)941 void MainWindow::updatedAnimDump(bool checked)
942 {
943 if (!checked) this->anim_dumping = false;
944 }
945
946 // Only called from animate_timer
updateTVal()947 void MainWindow::updateTVal()
948 {
949 if (this->anim_numsteps == 0) return;
950
951 if (windowActionHideCustomizer->isVisible()) {
952 if (this->parameterWidget->childHasFocus()) return;
953 }
954
955 if (this->anim_numsteps > 1) {
956 this->anim_step = (this->anim_step + 1) % this->anim_numsteps;
957 this->anim_tval = 1.0 * this->anim_step / this->anim_numsteps;
958 }
959 else if (this->anim_numsteps > 0) {
960 this->anim_step = 0;
961 this->anim_tval = 0.0;
962 }
963 const QString txt = QString::number(this->anim_tval, 'f', 5);
964 this->e_tval->setText(txt);
965 }
966
967 /*!
968 compiles the design. Calls compileDone() if anything was compiled
969 */
compile(bool reload,bool forcedone,bool rebuildParameterWidget)970 void MainWindow::compile(bool reload, bool forcedone, bool rebuildParameterWidget)
971 {
972 OpenSCAD::hardwarnings = Preferences::inst()->getValue("advanced/enableHardwarnings").toBool();
973 OpenSCAD::parameterCheck = Preferences::inst()->getValue("advanced/enableParameterCheck").toBool();
974 OpenSCAD::rangeCheck = Preferences::inst()->getValue("advanced/enableParameterRangeCheck").toBool();
975
976 try{
977 bool shouldcompiletoplevel = false;
978 bool didcompile = false;
979
980 compileErrors = 0;
981 compileWarnings = 0;
982
983 this->renderingTime.start();
984
985 // Reload checks the timestamp of the toplevel file and refreshes if necessary,
986 if (reload) {
987 // Refresh files if it has changed on disk
988 if (fileChangedOnDisk() && checkEditorModified()) {
989 shouldcompiletoplevel = true;
990 tabManager->refreshDocument();
991 if (Preferences::inst()->getValue("advanced/autoReloadRaise").toBool()) {
992 // reloading the 'same' document brings the 'old' one to front.
993 this->raise();
994 }
995 }
996 // If the file hasn't changed, we might still need to compile it
997 // if we haven't yet compiled the current text.
998 else {
999 auto current_doc = activeEditor->toPlainText();
1000 if (current_doc.size() && last_compiled_doc.size() == 0) {
1001 shouldcompiletoplevel = true;
1002 }
1003 }
1004 }
1005 else {
1006 shouldcompiletoplevel = true;
1007 }
1008
1009 if (this->parsed_module) {
1010 auto mtime = this->parsed_module->includesChanged();
1011 if (mtime > this->includes_mtime) {
1012 this->includes_mtime = mtime;
1013 shouldcompiletoplevel = true;
1014 }
1015 }
1016 // Parsing and dependency handling must run to completion even with stop on errors to prevent auto
1017 // reload picking up where it left off, thwarting the stop, so we turn off exceptions in PRINT.
1018 no_exceptions_for_warnings();
1019 if (shouldcompiletoplevel) {
1020 this->errorLogWidget->clearModel();
1021 if (activeEditor->isContentModified()) saveBackup();
1022 parseTopLevelDocument(rebuildParameterWidget);
1023 didcompile = true;
1024 }
1025
1026 if (didcompile && parser_error_pos != last_parser_error_pos) {
1027 if(last_parser_error_pos >= 0)
1028 emit unhighlightLastError();
1029 if (parser_error_pos >= 0)
1030 emit highlightError( parser_error_pos );
1031 last_parser_error_pos = parser_error_pos;
1032 }
1033
1034 if (this->root_module) {
1035 auto mtime = this->root_module->handleDependencies();
1036 if (mtime > this->deps_mtime) {
1037 this->deps_mtime = mtime;
1038 LOG(message_group::None,Location::NONE,"","Used file cache size: %1$d files",ModuleCache::instance()->size());
1039 didcompile = true;
1040 }
1041 }
1042
1043 // Had any errors in the parse that would have caused exceptions via PRINT.
1044 if(would_have_thrown())
1045 throw HardWarningException("");
1046 // If we're auto-reloading, listen for a cascade of changes by starting a timer
1047 // if something changed _and_ there are any external dependencies
1048 if (reload && didcompile && this->root_module) {
1049 if (this->root_module->hasIncludes() || this->root_module->usesLibraries()) {
1050 this->waitAfterReloadTimer->start();
1051 this->procevents = false;
1052 return;
1053 }
1054 }
1055
1056 compileDone(didcompile | forcedone);
1057 }catch(const HardWarningException&){
1058 exceptionCleanup();
1059 }
1060 }
1061
waitAfterReload()1062 void MainWindow::waitAfterReload()
1063 {
1064 no_exceptions_for_warnings();
1065 auto mtime = this->root_module->handleDependencies();
1066 auto stop = would_have_thrown();
1067 if (mtime > this->deps_mtime)
1068 this->deps_mtime = mtime;
1069 else
1070 if(!stop) {
1071 compile(true, true); // In case file itself or top-level includes changed during dependency updates
1072 return;
1073 }
1074 this->waitAfterReloadTimer->start();
1075 }
1076
on_toolButtonCompileResultClose_clicked()1077 void MainWindow::on_toolButtonCompileResultClose_clicked()
1078 {
1079 frameCompileResult->hide();
1080 }
1081
updateCompileResult()1082 void MainWindow::updateCompileResult()
1083 {
1084 if ((compileErrors == 0) && (compileWarnings == 0)) {
1085 frameCompileResult->hide();
1086 return;
1087 }
1088
1089 auto s = Settings::Settings::inst();
1090 if (!s->get(Settings::Settings::showWarningsIn3dView).toBool()) {
1091 return;
1092 }
1093
1094 QString msg;
1095 if (compileErrors > 0) {
1096 if (activeEditor->filepath.isEmpty()) {
1097 msg = QString(_("Compile error."));
1098 } else {
1099 QFileInfo fileInfo(activeEditor->filepath);
1100 msg = QString(_("Error while compiling '%1'.")).arg(fileInfo.fileName());
1101 }
1102 toolButtonCompileResultIcon->setIcon(QIcon(QString::fromUtf8(":/icons/information-icons-error.png")));
1103 } else {
1104 const char *fmt = ngettext("Compilation generated %1 warning.", "Compilation generated %1 warnings.", compileWarnings);
1105 msg = QString(fmt).arg(compileWarnings);
1106 toolButtonCompileResultIcon->setIcon(QIcon(QString::fromUtf8(":/icons/information-icons-warning.png")));
1107 }
1108 QFontMetrics fm(labelCompileResultMessage->font());
1109 int sizeIcon = std::max(12, std::min(32, fm.height()));
1110 int sizeClose = std::max(10, std::min(32, fm.height()) - 4);
1111 toolButtonCompileResultIcon->setIconSize(QSize(sizeIcon, sizeIcon));
1112 toolButtonCompileResultClose->setIconSize(QSize(sizeClose, sizeClose));
1113
1114 msg += _(" For details see the <a href=\"#errorlog\">error log</a> and <a href=\"#console\">console window</a>.");
1115 labelCompileResultMessage->setText(msg);
1116 frameCompileResult->show();
1117 }
1118
compileDone(bool didchange)1119 void MainWindow::compileDone(bool didchange)
1120 {
1121 OpenSCAD::hardwarnings = Preferences::inst()->getValue("advanced/enableHardwarnings").toBool();
1122 try{
1123 const char *callslot;
1124 if (didchange) {
1125 updateTemporalVariables();
1126 instantiateRoot();
1127 updateCompileResult();
1128 callslot = afterCompileSlot;
1129 }
1130 else {
1131 callslot = "compileEnded";
1132 }
1133
1134 this->procevents = false;
1135 QMetaObject::invokeMethod(this, callslot);
1136 }catch(const HardWarningException&){
1137 exceptionCleanup();
1138 }
1139 }
1140
compileEnded()1141 void MainWindow::compileEnded()
1142 {
1143 clearCurrentOutput();
1144 GuiLocker::unlock();
1145 if (designActionAutoReload->isChecked()) autoReloadTimer->start();
1146 }
1147
instantiateRoot()1148 void MainWindow::instantiateRoot()
1149 {
1150 // Go on and instantiate root_node, then call the continuation slot
1151
1152 // Invalidate renderers before we kill the CSG tree
1153 this->qglview->setRenderer(nullptr);
1154 #ifdef ENABLE_OPENCSG
1155 delete this->opencsgRenderer;
1156 this->opencsgRenderer = nullptr;
1157 #endif
1158 delete this->thrownTogetherRenderer;
1159 this->thrownTogetherRenderer = nullptr;
1160
1161 // Remove previous CSG tree
1162 delete this->absolute_root_node;
1163 this->absolute_root_node = nullptr;
1164
1165 this->csgRoot.reset();
1166 this->normalizedRoot.reset();
1167 this->root_products.reset();
1168
1169 this->root_node = nullptr;
1170 this->tree.setRoot(nullptr);
1171
1172 boost::filesystem::path doc(activeEditor->filepath.toStdString());
1173 this->tree.setDocumentPath(doc.remove_filename().string());
1174
1175 if (this->root_module) {
1176 // Evaluate CSG tree
1177 LOG(message_group::None,Location::NONE,"","Compiling design (CSG Tree generation)...");
1178 this->processEvents();
1179
1180 AbstractNode::resetIndexCounter();
1181
1182 // split these two lines - gcc 4.7 bug
1183 auto mi = ModuleInstantiation( "group" );
1184 this->root_inst = mi;
1185
1186 ContextHandle<FileContext> filectx{Context::create<FileContext>(top_ctx.ctx)};
1187 this->absolute_root_node = this->root_module->instantiateWithFileContext(filectx.ctx, &this->root_inst, nullptr);
1188 this->qglview->cam.updateView(filectx.ctx, false);
1189
1190 if (this->absolute_root_node) {
1191 // Do we have an explicit root node (! modifier)?
1192 const Location *nextLocation = nullptr;
1193 if (!(this->root_node = find_root_tag(this->absolute_root_node, &nextLocation))) {
1194 this->root_node = this->absolute_root_node;
1195 }
1196 if (nextLocation) {
1197 LOG(message_group::None,*nextLocation,top_ctx->documentPath(),"More than one Root Modifier (!)");
1198 }
1199
1200 // FIXME: Consider giving away ownership of root_node to the Tree, or use reference counted pointers
1201 this->tree.setRoot(this->root_node);
1202 }
1203 }
1204
1205 if (!this->root_node) {
1206 if (parser_error_pos < 0) {
1207 LOG(message_group::Error,Location::NONE,"","Compilation failed! (no top level object found)");
1208 } else {
1209 LOG(message_group::Error,Location::NONE,"","Compilation failed!");
1210 }
1211 LOG(message_group::None,Location::NONE,""," ");
1212 this->processEvents();
1213 }
1214 }
1215
1216 /*!
1217 Generates CSG tree for OpenCSG evaluation.
1218 Assumes that the design has been parsed and evaluated (this->root_node is set)
1219 */
compileCSG()1220 void MainWindow::compileCSG()
1221 {
1222 OpenSCAD::hardwarnings = Preferences::inst()->getValue("advanced/enableHardwarnings").toBool();
1223 try{
1224 assert(this->root_node);
1225 LOG(message_group::None,Location::NONE,"","Compiling design (CSG Products generation)...");
1226 this->processEvents();
1227
1228 // Main CSG evaluation
1229 this->progresswidget = new ProgressWidget(this);
1230 connect(this->progresswidget, SIGNAL(requestShow()), this, SLOT(showProgress()));
1231
1232 #ifdef ENABLE_CGAL
1233 GeometryEvaluator geomevaluator(this->tree);
1234 #else
1235 // FIXME: Will we support this?
1236 #endif
1237 #ifdef ENABLE_OPENCSG
1238 CSGTreeEvaluator csgrenderer(this->tree, &geomevaluator);
1239 #endif
1240
1241 progress_report_prep(this->root_node, report_func, this);
1242 try {
1243 #ifdef ENABLE_OPENCSG
1244 this->processEvents();
1245 this->csgRoot = csgrenderer.buildCSGTree(*root_node);
1246 #endif
1247 RenderStatistic::printCacheStatistic();
1248 this->processEvents();
1249 }
1250 catch (const ProgressCancelException &) {
1251 LOG(message_group::None,Location::NONE,"","CSG generation cancelled.");
1252 }catch(const HardWarningException &){
1253 LOG(message_group::None,Location::NONE,"","CSG generation cancelled due to hardwarning being enabled.");
1254 }
1255 progress_report_fin();
1256 updateStatusBar(nullptr);
1257
1258 LOG(message_group::None,Location::NONE,"","Compiling design (CSG Products normalization)...");
1259 this->processEvents();
1260
1261 size_t normalizelimit = 2 * Preferences::inst()->getValue("advanced/openCSGLimit").toUInt();
1262 CSGTreeNormalizer normalizer(normalizelimit);
1263
1264 if (this->csgRoot) {
1265 this->normalizedRoot = normalizer.normalize(this->csgRoot);
1266 if (this->normalizedRoot) {
1267 this->root_products.reset(new CSGProducts());
1268 this->root_products->import(this->normalizedRoot);
1269 }
1270 else {
1271 this->root_products.reset();
1272 LOG(message_group::Warning,Location::NONE,"","CSG normalization resulted in an empty tree");
1273 this->processEvents();
1274 }
1275 }
1276
1277 const std::vector<shared_ptr<CSGNode> > &highlight_terms = csgrenderer.getHighlightNodes();
1278 if (highlight_terms.size() > 0) {
1279 LOG(message_group::None,Location::NONE,"","Compiling highlights (%1$d CSG Trees)...",highlight_terms.size());
1280 this->processEvents();
1281
1282 this->highlights_products.reset(new CSGProducts());
1283 for (unsigned int i = 0; i < highlight_terms.size(); ++i) {
1284 auto nterm = normalizer.normalize(highlight_terms[i]);
1285 if (nterm) {
1286 this->highlights_products->import(nterm);
1287 }
1288 }
1289 }
1290 else {
1291 this->highlights_products.reset();
1292 }
1293
1294 const auto &background_terms = csgrenderer.getBackgroundNodes();
1295 if (background_terms.size() > 0) {
1296 LOG(message_group::None,Location::NONE,"","Compiling background (%1$d CSG Trees)...",background_terms.size());
1297 this->processEvents();
1298
1299 this->background_products.reset(new CSGProducts());
1300 for (unsigned int i = 0; i < background_terms.size(); ++i) {
1301 auto nterm = normalizer.normalize(background_terms[i]);
1302 if (nterm) {
1303 this->background_products->import(nterm);
1304 }
1305 }
1306 }
1307 else {
1308 this->background_products.reset();
1309 }
1310
1311 if (this->root_products &&
1312 (this->root_products->size() >
1313 Preferences::inst()->getValue("advanced/openCSGLimit").toUInt())) {
1314 LOG(message_group::UI_Warning,Location::NONE,"","Normalized tree has %1$d elements!",this->root_products->size());
1315 LOG(message_group::UI_Warning,Location::NONE,"","OpenCSG rendering has been disabled.");
1316 }
1317 #ifdef ENABLE_OPENCSG
1318 else {
1319 LOG(message_group::None,Location::NONE,"","Normalized tree has %1$d elements!",
1320 (this->root_products ? this->root_products->size() : 0));
1321 this->opencsgRenderer = new OpenCSGRenderer(this->root_products,
1322 this->highlights_products,
1323 this->background_products);
1324 }
1325 #endif
1326 this->thrownTogetherRenderer = new ThrownTogetherRenderer(this->root_products,
1327 this->highlights_products,
1328 this->background_products);
1329 LOG(message_group::None,Location::NONE,"","Compile and preview finished.");
1330 std::chrono::milliseconds ms{this->renderingTime.elapsed()};
1331 RenderStatistic::printRenderingTime(ms);
1332 this->processEvents();
1333 }catch(const HardWarningException&){
1334 exceptionCleanup();
1335 }
1336 }
1337
actionOpen()1338 void MainWindow::actionOpen()
1339 {
1340 auto fileInfoList = UIUtils::openFiles(this);
1341 for(int i = 0; i < fileInfoList.size(); ++i)
1342 {
1343 if (!fileInfoList[i].exists()) {
1344 return;
1345 }
1346 tabManager->open(fileInfoList[i].filePath());
1347 }
1348 }
1349
actionNewWindow()1350 void MainWindow::actionNewWindow()
1351 {
1352 new MainWindow(QStringList());
1353 }
1354
actionOpenWindow()1355 void MainWindow::actionOpenWindow()
1356 {
1357 auto fileInfoList = UIUtils::openFiles(this);
1358 for(int i = 0; i < fileInfoList.size(); ++i)
1359 {
1360 if (!fileInfoList[i].exists()) {
1361 return;
1362 }
1363 new MainWindow(QStringList(fileInfoList[i].filePath()));
1364 }
1365 }
1366
actionOpenRecent()1367 void MainWindow::actionOpenRecent()
1368 {
1369 auto action = qobject_cast<QAction *>(sender());
1370 tabManager->open(action->data().toString());
1371 }
1372
clearRecentFiles()1373 void MainWindow::clearRecentFiles()
1374 {
1375 QSettingsCached settings; // already set up properly via main.cpp
1376 QStringList files;
1377 settings.setValue("recentFileList", files);
1378
1379 updateRecentFileActions();
1380 }
1381
updateRecentFileActions()1382 void MainWindow::updateRecentFileActions()
1383 {
1384 auto files = UIUtils::recentFiles();
1385
1386 for (int i = 0; i < files.size(); ++i) {
1387 this->actionRecentFile[i]->setText(QFileInfo(files[i]).fileName().replace("&", "&&"));
1388 this->actionRecentFile[i]->setData(files[i]);
1389 this->actionRecentFile[i]->setVisible(true);
1390 }
1391 for (int i = files.size(); i < UIUtils::maxRecentFiles; ++i) {
1392 this->actionRecentFile[i]->setVisible(false);
1393 }
1394 }
1395
show_examples()1396 void MainWindow::show_examples()
1397 {
1398 bool found_example = false;
1399
1400 for (const auto &cat : UIUtils::exampleCategories()) {
1401 auto examples = UIUtils::exampleFiles(cat);
1402 auto menu = this->menuExamples->addMenu(gettext(cat.toStdString().c_str()));
1403
1404 for (const auto &ex : examples) {
1405 auto openAct = new QAction(ex.fileName().replace("&", "&&"), this);
1406 connect(openAct, SIGNAL(triggered()), this, SLOT(actionOpenExample()));
1407 menu->addAction(openAct);
1408 openAct->setData(ex.canonicalFilePath());
1409 found_example = true;
1410 }
1411 }
1412
1413 if (!found_example) {
1414 delete this->menuExamples;
1415 this->menuExamples = nullptr;
1416 }
1417 }
1418
actionOpenExample()1419 void MainWindow::actionOpenExample()
1420 {
1421 const auto action = qobject_cast<QAction *>(sender());
1422 if (action) {
1423 const auto &path = action->data().toString();
1424 tabManager->open(path);
1425 }
1426 }
1427
writeBackup(QFile * file)1428 void MainWindow::writeBackup(QFile *file)
1429 {
1430 // see MainWindow::saveBackup()
1431 file->resize(0);
1432 QTextStream writer(file);
1433 writer.setCodec("UTF-8");
1434 writer << activeEditor->toPlainText();
1435 this->parameterWidget->writeBackupFile(file->fileName());
1436
1437 LOG(message_group::None,Location::NONE,"","Saved backup file: %1$s", file->fileName().toUtf8().constData());
1438 }
1439
saveBackup()1440 void MainWindow::saveBackup()
1441 {
1442 auto path = PlatformUtils::backupPath();
1443 if ((!fs::exists(path)) && (!PlatformUtils::createBackupPath())) {
1444 LOG(message_group::UI_Warning,Location::NONE,"","Cannot create backup path: %1$s",path);
1445 return;
1446 }
1447
1448 auto backupPath = QString::fromLocal8Bit(path.c_str());
1449 if (!backupPath.endsWith("/")) backupPath.append("/");
1450
1451 QString basename = "unsaved";
1452 if (!activeEditor->filepath.isEmpty()) {
1453 auto fileInfo = QFileInfo(activeEditor->filepath);
1454 basename = fileInfo.baseName();
1455 }
1456
1457 if (!this->tempFile) {
1458 this->tempFile = new QTemporaryFile(backupPath.append(basename + "-backup-XXXXXXXX.scad"));
1459 }
1460
1461 if ((!this->tempFile->isOpen()) && (! this->tempFile->open())) {
1462 LOG(message_group::UI_Warning,Location::NONE,"","Failed to create backup file");
1463 return;
1464 }
1465 return writeBackup(this->tempFile);
1466 }
1467
actionSave()1468 void MainWindow::actionSave()
1469 {
1470 tabManager->save(activeEditor);
1471 }
1472
actionSaveAs()1473 void MainWindow::actionSaveAs()
1474 {
1475 tabManager->saveAs(activeEditor);
1476 }
1477
actionShowLibraryFolder()1478 void MainWindow::actionShowLibraryFolder()
1479 {
1480 auto path = PlatformUtils::userLibraryPath();
1481 if (!fs::exists(path)) {
1482 LOG(message_group::UI_Warning,Location::NONE,"","Library path %1$s doesn't exist. Creating",path);
1483 if (!PlatformUtils::createUserLibraryPath()) {
1484 LOG(message_group::UI_Error,Location::NONE,"","Cannot create library path: %1$s",path);
1485 }
1486 }
1487 auto url = QString::fromStdString(path);
1488 LOG(message_group::None,Location::NONE,"","Opening file browser for %1$s",url.toStdString());
1489 QDesktopServices::openUrl(QUrl::fromLocalFile(url));
1490 }
1491
actionReload()1492 void MainWindow::actionReload()
1493 {
1494 if (checkEditorModified()) {
1495 fileChangedOnDisk(); // force cached autoReloadId to update
1496 tabManager->refreshDocument();
1497 }
1498 }
1499
copyViewportTranslation()1500 void MainWindow::copyViewportTranslation()
1501 {
1502 const auto vpt = qglview->cam.getVpt();
1503 const QString txt = QString("[ %1, %2, %3 ]")
1504 .arg(vpt.x(), 0, 'f', 2)
1505 .arg(vpt.y(), 0, 'f', 2)
1506 .arg(vpt.z(), 0, 'f', 2);
1507 QApplication::clipboard()->setText(txt);
1508 }
1509
copyViewportRotation()1510 void MainWindow::copyViewportRotation()
1511 {
1512 const auto vpr = qglview->cam.getVpr();
1513 const QString txt = QString("[ %1, %2, %3 ]")
1514 .arg(vpr.x(), 0, 'f', 2)
1515 .arg(vpr.y(), 0, 'f', 2)
1516 .arg(vpr.z(), 0, 'f', 2);
1517 QApplication::clipboard()->setText(txt);
1518 }
1519
copyViewportDistance()1520 void MainWindow::copyViewportDistance()
1521 {
1522 const QString txt = QString::number(qglview->cam.zoomValue(), 'f', 2);
1523 QApplication::clipboard()->setText(txt);
1524 }
1525
copyViewportFov()1526 void MainWindow::copyViewportFov()
1527 {
1528 const QString txt = QString::number(qglview->cam.fovValue(), 'f', 2);
1529 QApplication::clipboard()->setText(txt);
1530 }
1531
getTranslation() const1532 QList<double> MainWindow::getTranslation() const
1533 {
1534 QList<double> ret;
1535 ret.append(qglview->cam.object_trans.x());
1536 ret.append(qglview->cam.object_trans.y());
1537 ret.append(qglview->cam.object_trans.z());
1538 return ret;
1539 }
1540
getRotation() const1541 QList<double> MainWindow::getRotation() const
1542 {
1543 QList<double> ret;
1544 ret.append(qglview->cam.object_rot.x());
1545 ret.append(qglview->cam.object_rot.y());
1546 ret.append(qglview->cam.object_rot.z());
1547 return ret;
1548 }
1549
hideFind()1550 void MainWindow::hideFind()
1551 {
1552 find_panel->hide();
1553 activeEditor->findState = TabManager::FIND_HIDDEN;
1554 editActionFindNext->setEnabled(false);
1555 editActionFindPrevious->setEnabled(false);
1556 this->findInputField->setFindCount(activeEditor->updateFindIndicators(this->findInputField->text(), false));
1557 this->processEvents();
1558 }
1559
showFind()1560 void MainWindow::showFind()
1561 {
1562 this->findInputField->setFindCount(activeEditor->updateFindIndicators(this->findInputField->text()));
1563 this->processEvents();
1564 findTypeComboBox->setCurrentIndex(0);
1565 replaceInputField->hide();
1566 replaceButton->hide();
1567 replaceAllButton->hide();
1568 //replaceLabel->setVisible(false);
1569 find_panel->show();
1570 activeEditor->findState = TabManager::FIND_VISIBLE;
1571 editActionFindNext->setEnabled(true);
1572 editActionFindPrevious->setEnabled(true);
1573 if (!activeEditor->selectedText().isEmpty()) {
1574 findInputField->setText(activeEditor->selectedText());
1575 }
1576 findInputField->setFocus();
1577 findInputField->selectAll();
1578 }
1579
findString(QString textToFind)1580 void MainWindow::findString(QString textToFind)
1581 {
1582 this->findInputField->setFindCount(activeEditor->updateFindIndicators(textToFind));
1583 this->processEvents();
1584 activeEditor->find(textToFind);
1585 }
1586
showFindAndReplace()1587 void MainWindow::showFindAndReplace()
1588 {
1589 this->findInputField->setFindCount(activeEditor->updateFindIndicators(this->findInputField->text()));
1590 this->processEvents();
1591 findTypeComboBox->setCurrentIndex(1);
1592 replaceInputField->show();
1593 replaceButton->show();
1594 replaceAllButton->show();
1595 //replaceLabel->setVisible(true);
1596 find_panel->show();
1597 activeEditor->findState = TabManager::FIND_REPLACE_VISIBLE;
1598 editActionFindNext->setEnabled(true);
1599 editActionFindPrevious->setEnabled(true);
1600 if (!activeEditor->selectedText().isEmpty()) {
1601 findInputField->setText(activeEditor->selectedText());
1602 }
1603 findInputField->setFocus();
1604 findInputField->selectAll();
1605 }
1606
selectFindType(int type)1607 void MainWindow::selectFindType(int type)
1608 {
1609 if (type == 0) showFind();
1610 if (type == 1) showFindAndReplace();
1611 }
1612
replace()1613 void MainWindow::replace()
1614 {
1615 activeEditor->replaceSelectedText(this->replaceInputField->text());
1616 activeEditor->find(this->findInputField->text());
1617 }
1618
replaceAll()1619 void MainWindow::replaceAll()
1620 {
1621 activeEditor->replaceAll(this->findInputField->text(), this->replaceInputField->text());
1622 }
1623
convertTabsToSpaces()1624 void MainWindow::convertTabsToSpaces()
1625 {
1626 const auto text = activeEditor->toPlainText();
1627
1628 QString converted;
1629
1630 int cnt = 4;
1631 for (int idx = 0; idx < text.length(); ++idx) {
1632 auto c = text.at(idx);
1633 if (c == '\t') {
1634 for (; cnt > 0; cnt--) {
1635 converted.append(' ');
1636 }
1637 } else {
1638 converted.append(c);
1639 }
1640 if (cnt <= 0 || c == '\n') {
1641 cnt = 5;
1642 }
1643 cnt--;
1644 }
1645 activeEditor->setText(converted);
1646 }
1647
findNext()1648 void MainWindow::findNext()
1649 {
1650 activeEditor->find(this->findInputField->text(), true);
1651 }
1652
findPrev()1653 void MainWindow::findPrev()
1654 {
1655 activeEditor->find(this->findInputField->text(), true, true);
1656 }
1657
useSelectionForFind()1658 void MainWindow::useSelectionForFind()
1659 {
1660 findInputField->setText(activeEditor->selectedText());
1661 }
1662
updateFindBuffer(QString s)1663 void MainWindow::updateFindBuffer(QString s)
1664 {
1665 QApplication::clipboard()->setText(s, QClipboard::FindBuffer);
1666 }
1667
findBufferChanged()1668 void MainWindow::findBufferChanged()
1669 {
1670 auto t = QApplication::clipboard()->text(QClipboard::FindBuffer);
1671 // The convention seems to be to not update the search field if the findbuffer is empty
1672 if (!t.isEmpty()) {
1673 findInputField->setText(t);
1674 }
1675 }
1676
event(QEvent * event)1677 bool MainWindow::event(QEvent* event) {
1678 if (event->type() == InputEvent::eventType) {
1679 InputEvent *inputEvent = dynamic_cast<InputEvent *>(event);
1680 if (inputEvent) {
1681 inputEvent->deliver(this);
1682 }
1683 event->accept();
1684 return true;
1685 }
1686 return QMainWindow::event(event);
1687 }
1688
eventFilter(QObject * obj,QEvent * event)1689 bool MainWindow::eventFilter(QObject* obj, QEvent *event)
1690 {
1691 if (obj == find_panel) {
1692 if (event->type() == QEvent::KeyPress) {
1693 auto keyEvent = static_cast<QKeyEvent*>(event);
1694 if (keyEvent->key() == Qt::Key_Escape) {
1695 this->hideFind();
1696 return true;
1697 }
1698 }
1699 return false;
1700 }
1701 return QMainWindow::eventFilter(obj, event);
1702 }
1703
updateTemporalVariables()1704 void MainWindow::updateTemporalVariables()
1705 {
1706 this->top_ctx->set_variable("$t", Value(this->anim_tval));
1707 auto camVpt = qglview->cam.getVpt();
1708 this->top_ctx->set_variable("$vpt", Value(VectorType(camVpt.x(), camVpt.y(), camVpt.z())));
1709 auto camVpr = qglview->cam.getVpr();
1710 top_ctx->set_variable("$vpr", Value(VectorType(camVpr.x(), camVpr.y(), camVpr.z())));
1711 top_ctx->set_variable("$vpd", Value(qglview->cam.zoomValue()));
1712 top_ctx->set_variable("$vpf", Value(qglview->cam.fovValue()));
1713 }
1714
1715 /*!
1716 Returns true if the current document is a file on disk and that file has new content.
1717 Returns false if a file on disk has disappeared or if we haven't yet saved.
1718 */
fileChangedOnDisk()1719 bool MainWindow::fileChangedOnDisk()
1720 {
1721 if (!activeEditor->filepath.isEmpty()) {
1722 struct stat st;
1723 memset(&st, 0, sizeof(struct stat));
1724 bool valid = (stat(activeEditor->filepath.toLocal8Bit(), &st) == 0);
1725 // If file isn't there, just return and use current editor text
1726 if (!valid) return false;
1727
1728 auto newid = str(boost::format("%x.%x") % st.st_mtime % st.st_size);
1729
1730 if (newid != activeEditor->autoReloadId) {
1731 activeEditor->autoReloadId = newid;
1732 return true;
1733 }
1734 }
1735 return false;
1736 }
1737
1738 /*!
1739 Returns true if anything was compiled.
1740 */
parseTopLevelDocument(bool rebuildParameterWidget)1741 void MainWindow::parseTopLevelDocument(bool rebuildParameterWidget)
1742 {
1743 bool reloadSettings = customizerEditor != activeEditor;
1744 customizerEditor = nullptr;
1745 this->parameterWidget->setEnabled(false);
1746 resetSuppressedMessages();
1747
1748 this->last_compiled_doc = activeEditor->toPlainText();
1749
1750 auto fulltext =
1751 std::string(this->last_compiled_doc.toUtf8().constData()) +
1752 "\n\x03\n" + commandline_commands;
1753
1754 auto fnameba = activeEditor->filepath.toLocal8Bit();
1755 const char* fname = activeEditor->filepath.isEmpty() ? "" : fnameba;
1756 delete this->parsed_module;
1757 this->root_module = parse(this->parsed_module, fulltext, fname, fname, false) ? this->parsed_module : nullptr;
1758
1759 if (this->root_module!=nullptr) {
1760 if (reloadSettings && !activeEditor->filepath.isEmpty()) {
1761 this->parameterWidget->readFile(activeEditor->filepath);
1762 }
1763 //add parameters as annotation in AST
1764 CommentParser::collectParameters(fulltext,this->root_module);
1765 this->parameterWidget->setParameters(this->root_module,rebuildParameterWidget);
1766 this->parameterWidget->applyParameters(this->root_module);
1767 customizerEditor = activeEditor;
1768 this->parameterWidget->setEnabled(true);
1769 this->activeEditor->setIndicator(this->root_module->indicatorData);
1770 }
1771 }
1772
changeParameterWidget()1773 void MainWindow::changeParameterWidget()
1774 {
1775 windowActionHideCustomizer->setVisible(true);
1776 }
1777
checkAutoReload()1778 void MainWindow::checkAutoReload()
1779 {
1780 if (!activeEditor->filepath.isEmpty()) {
1781 actionReloadRenderPreview();
1782 }
1783 }
1784
autoReloadSet(bool on)1785 void MainWindow::autoReloadSet(bool on)
1786 {
1787 QSettingsCached settings;
1788 settings.setValue("design/autoReload",designActionAutoReload->isChecked());
1789 if (on) {
1790 autoReloadTimer->start(autoReloadPollingPeriodMS);
1791 } else {
1792 autoReloadTimer->stop();
1793 }
1794 }
1795
checkEditorModified()1796 bool MainWindow::checkEditorModified()
1797 {
1798 if (activeEditor->isContentModified()) {
1799 auto ret = QMessageBox::warning(this, _("Application"),
1800 _("The document has been modified.\n"
1801 "Do you really want to reload the file?"),
1802 QMessageBox::Yes | QMessageBox::No);
1803 if (ret != QMessageBox::Yes) {
1804 return false;
1805 }
1806 }
1807 return true;
1808 }
1809
actionReloadRenderPreview()1810 void MainWindow::actionReloadRenderPreview()
1811 {
1812 if (GuiLocker::isLocked()) return;
1813 GuiLocker::lock();
1814 autoReloadTimer->stop();
1815 setCurrentOutput();
1816
1817 this->afterCompileSlot = "csgReloadRender";
1818 this->procevents = true;
1819 this->top_ctx->set_variable("$preview", Value(true));
1820 compile(true);
1821 }
1822
csgReloadRender()1823 void MainWindow::csgReloadRender()
1824 {
1825 if (this->root_node) compileCSG();
1826
1827 // Go to non-CGAL view mode
1828 if (viewActionThrownTogether->isChecked()) {
1829 viewModeThrownTogether();
1830 }
1831 else {
1832 #ifdef ENABLE_OPENCSG
1833 viewModePreview();
1834 #else
1835 viewModeThrownTogether();
1836 #endif
1837 }
1838 compileEnded();
1839 }
1840
prepareCompile(const char * afterCompileSlot,bool procevents,bool preview)1841 void MainWindow::prepareCompile(const char *afterCompileSlot, bool procevents, bool preview)
1842 {
1843 autoReloadTimer->stop();
1844 setCurrentOutput();
1845 LOG(message_group::None, Location::NONE, "", " ");
1846 LOG(message_group::None, Location::NONE, "", "Parsing design (AST generation)...");
1847 this->processEvents();
1848 this->afterCompileSlot = afterCompileSlot;
1849 this->procevents = procevents;
1850 this->top_ctx->set_variable("$preview", Value(preview));
1851 }
1852
actionRenderPreview(bool rebuildParameterWidget)1853 void MainWindow::actionRenderPreview(bool rebuildParameterWidget)
1854 {
1855 static bool preview_requested;
1856
1857 preview_requested=true;
1858 if (GuiLocker::isLocked()) return;
1859 GuiLocker::lock();
1860 preview_requested=false;
1861
1862 prepareCompile("csgRender", !viewActionAnimate->isChecked(), true);
1863 compile(false, false, rebuildParameterWidget);
1864 if (preview_requested) {
1865 // if the action was called when the gui was locked, we must request it one more time
1866 // however, it's not possible to call it directly NOR make the loop
1867 // it must be called from the mainloop
1868 QTimer::singleShot(0, this, SLOT(actionRenderPreview()));
1869 }
1870 }
1871
csgRender()1872 void MainWindow::csgRender()
1873 {
1874 if (this->root_node) compileCSG();
1875
1876 // Go to non-CGAL view mode
1877 if (viewActionThrownTogether->isChecked()) {
1878 viewModeThrownTogether();
1879 }
1880 else {
1881 #ifdef ENABLE_OPENCSG
1882 viewModePreview();
1883 #else
1884 viewModeThrownTogether();
1885 #endif
1886 }
1887
1888 if (e_dump->isChecked() && animate_timer->isActive()) {
1889 if (anim_dumping && anim_dump_start_step == anim_step) {
1890 anim_dumping=false;
1891 e_dump->setChecked(false);
1892 } else {
1893 if (!anim_dumping) {
1894 anim_dumping = true;
1895 anim_dump_start_step = anim_step;
1896 }
1897 // Force reading from front buffer. Some configurations will read from the back buffer here.
1898 glReadBuffer(GL_FRONT);
1899 QImage img = this->qglview->grabFrameBuffer();
1900 QString filename = QString("frame%1.png").arg(this->anim_step, 5, 10, QChar('0'));
1901 img.save(filename, "PNG");
1902 }
1903 }
1904
1905 compileEnded();
1906 }
1907
action3DPrint()1908 void MainWindow::action3DPrint()
1909 {
1910 #ifdef ENABLE_3D_PRINTING
1911 if (GuiLocker::isLocked()) return;
1912 GuiLocker lock;
1913
1914 setCurrentOutput();
1915
1916 //Make sure we can export:
1917 const unsigned int dim = 3;
1918 if (!canExport(dim)) return;
1919
1920 const auto printService = PrintService::inst();
1921 auto printInitDialog = new PrintInitDialog();
1922 auto printInitResult = printInitDialog->exec();
1923 printInitDialog->deleteLater();
1924 if (printInitResult == QDialog::Rejected) {
1925 return;
1926 }
1927
1928 const auto selectedService = printInitDialog->getResult();
1929 Preferences::Preferences::inst()->updateGUI();
1930
1931 switch (selectedService) {
1932 case print_service_t::PRINT_SERVICE:
1933 LOG(message_group::None,Location::NONE,"","Sending design to print service %1$s...",printService->getDisplayName().toStdString());
1934 sendToPrintService();
1935 break;
1936 case print_service_t::OCTOPRINT:
1937 LOG(message_group::None,Location::NONE,"","Sending design to OctoPrint...");
1938 sendToOctoPrint();
1939 break;
1940 default:
1941 break;
1942 }
1943 #endif
1944 }
1945
1946 namespace {
1947
createExportInfo(FileFormat format,const QString & exportFilename,const QString & sourceFilePath)1948 ExportInfo createExportInfo(FileFormat format, const QString& exportFilename, const QString& sourceFilePath)
1949 {
1950 const QFileInfo info(sourceFilePath);
1951
1952 ExportInfo exportInfo;
1953 exportInfo.format = format;
1954 exportInfo.name2open = exportFilename.toLocal8Bit().constData();
1955 exportInfo.name2display = exportFilename.toUtf8().toStdString();
1956 exportInfo.sourceFilePath = sourceFilePath.toUtf8().toStdString();
1957 exportInfo.sourceFileName = info.fileName().toUtf8().toStdString();
1958 exportInfo.useStdOut = false;
1959 return exportInfo;
1960 }
1961
1962 }
1963
sendToOctoPrint()1964 void MainWindow::sendToOctoPrint()
1965 {
1966 #ifdef ENABLE_3D_PRINTING
1967 OctoPrint octoPrint;
1968
1969 if (octoPrint.url().trimmed().isEmpty()) {
1970 LOG(message_group::Error,Location::NONE,"","OctoPrint connection not configured. Please check preferences.");
1971 return;
1972 }
1973
1974 Settings::Settings *s = Settings::Settings::inst();
1975 const QString fileFormat = QString::fromStdString(s->get(Settings::Settings::octoPrintFileFormat).toString());
1976 FileFormat exportFileFormat{FileFormat::STL};
1977 if (fileFormat == "OFF") {
1978 exportFileFormat = FileFormat::OFF;
1979 } else if (fileFormat == "ASCIISTL") {
1980 exportFileFormat = FileFormat::ASCIISTL;
1981 } else if (fileFormat == "AMF") {
1982 exportFileFormat = FileFormat::AMF;
1983 } else if (fileFormat == "3MF") {
1984 exportFileFormat = FileFormat::_3MF;
1985 } else {
1986 exportFileFormat = FileFormat::STL;
1987 }
1988
1989 QTemporaryFile exportFile{QDir::temp().filePath("OpenSCAD.XXXXXX." + fileFormat.toLower())};
1990 if (!exportFile.open()) {
1991 LOG(message_group::None,Location::NONE,"","Could not open temporary file.");
1992 return;
1993 }
1994 const QString exportFileName = exportFile.fileName();
1995 exportFile.close();
1996
1997 QString userFileName;
1998 if (activeEditor->filepath.isEmpty()) {
1999 userFileName = exportFileName;
2000 } else {
2001 QFileInfo fileInfo{activeEditor->filepath};
2002 userFileName = fileInfo.baseName() + "." + fileFormat.toLower();
2003 }
2004
2005 ExportInfo exportInfo = createExportInfo(exportFileFormat, exportFileName, activeEditor->filepath);
2006 exportFileByName(this->root_geom, exportInfo);
2007
2008 try {
2009 this->progresswidget = new ProgressWidget(this);
2010 connect(this->progresswidget, SIGNAL(requestShow()), this, SLOT(showProgress()));
2011 const QString fileUrl = octoPrint.upload(exportFileName, userFileName, [this](double v) -> bool { return network_progress_func(v); });
2012
2013 const std::string action = s->get(Settings::Settings::octoPrintAction).toString();
2014 if (action == "upload") {
2015 return;
2016 }
2017
2018 const QString slicer = QString::fromStdString(s->get(Settings::Settings::octoPrintSlicerEngine).toString());
2019 const QString profile = QString::fromStdString(s->get(Settings::Settings::octoPrintSlicerProfile).toString());
2020 octoPrint.slice(fileUrl, slicer, profile, action != "slice", action == "print");
2021 } catch (const NetworkException& e) {
2022 LOG(message_group::Error,Location::NONE,"","%1$s",e.getErrorMessage());
2023 }
2024
2025 updateStatusBar(nullptr);
2026 #endif
2027 }
2028
sendToPrintService()2029 void MainWindow::sendToPrintService()
2030 {
2031 #ifdef ENABLE_3D_PRINTING
2032 //Keeps track of how many times we've exported and tries to create slightly unique filenames.
2033 //Not mission critical, since non-unique file names are fine for the API, just harder to
2034 //differentiate between in customer support later.
2035 static unsigned int printCounter = 0;
2036
2037 QTemporaryFile exportFile;
2038 if (!exportFile.open()) {
2039 LOG(message_group::Error,Location::NONE,"","Could not open temporary file.");
2040 return;
2041 }
2042 const QString exportFilename = exportFile.fileName();
2043
2044 //Render the stl to a temporary file:
2045 ExportInfo exportInfo = createExportInfo(FileFormat::STL, exportFilename, activeEditor->filepath);
2046 exportFileByName(this->root_geom, exportInfo);
2047
2048 //Create a name that the order process will use to refer to the file. Base it off of the project name
2049 QString userFacingName = "unsaved.stl";
2050 if (!activeEditor->filepath.isEmpty()) {
2051 const QString baseName = QFileInfo(activeEditor->filepath).baseName();
2052 userFacingName = QString{"%1_%2.stl"}.arg(baseName).arg(printCounter++);
2053 }
2054
2055 QFile file(exportFilename);
2056 if (!file.open(QIODevice::ReadOnly)) {
2057 LOG(message_group::Error,Location::NONE,"","Unable to open exported STL file.");
2058 return;
2059 }
2060 const QString fileContentBase64 = file.readAll().toBase64();
2061
2062 if (fileContentBase64.length() > PrintService::inst()->getFileSizeLimit()) {
2063 const auto msg = QString{_("Exported design exceeds the service upload limit of (%1 MB).")}.arg(PrintService::inst()->getFileSizeLimitMB());
2064 QMessageBox::warning(this, _("Upload Error"), msg, QMessageBox::Ok);
2065 LOG(message_group::Error,Location::NONE,"","%1$s",msg.toStdString());
2066 return;
2067 }
2068
2069 //Upload the file to the 3D Printing server and get the corresponding url to see it.
2070 //The result is put in partUrl.
2071 try
2072 {
2073 this->progresswidget = new ProgressWidget(this);
2074 connect(this->progresswidget, SIGNAL(requestShow()), this, SLOT(showProgress()));
2075 const QString partUrl = PrintService::inst()->upload(userFacingName, fileContentBase64, [this](double v) -> bool { return network_progress_func(v); });
2076 QDesktopServices::openUrl(QUrl{partUrl});
2077 } catch (const NetworkException& e) {
2078 LOG(message_group::Error,Location::NONE,"","%1$s",e.getErrorMessage());
2079 }
2080
2081 updateStatusBar(nullptr);
2082 #endif
2083 }
2084
2085 #ifdef ENABLE_CGAL
2086
actionRender()2087 void MainWindow::actionRender()
2088 {
2089 if (GuiLocker::isLocked()) return;
2090 GuiLocker::lock();
2091
2092 prepareCompile("cgalRender", true, false);
2093 compile(false);
2094 }
2095
cgalRender()2096 void MainWindow::cgalRender()
2097 {
2098 if (!this->root_module || !this->root_node) {
2099 compileEnded();
2100 return;
2101 }
2102
2103 this->qglview->setRenderer(nullptr);
2104 delete this->cgalRenderer;
2105 this->cgalRenderer = nullptr;
2106 this->root_geom.reset();
2107
2108 LOG(message_group::None,Location::NONE,"","Rendering Polygon Mesh using CGAL...");
2109
2110 this->progresswidget = new ProgressWidget(this);
2111 connect(this->progresswidget, SIGNAL(requestShow()), this, SLOT(showProgress()));
2112
2113 progress_report_prep(this->root_node, report_func, this);
2114
2115 this->cgalworker->start(this->tree);
2116 }
2117
actionRenderDone(shared_ptr<const Geometry> root_geom)2118 void MainWindow::actionRenderDone(shared_ptr<const Geometry> root_geom)
2119 {
2120 progress_report_fin();
2121 std::chrono::milliseconds ms{this->renderingTime.elapsed()};
2122 RenderStatistic::printCacheStatistic();
2123 RenderStatistic::printRenderingTime(ms);
2124 if (root_geom) {
2125 if (!root_geom->isEmpty()) {
2126 RenderStatistic().print(*root_geom);
2127 }
2128 LOG(message_group::None,Location::NONE,"","Rendering finished.");
2129
2130 this->root_geom = root_geom;
2131 this->cgalRenderer = new CGALRenderer(root_geom);
2132 // Go to CGAL view mode
2133 if (viewActionWireframe->isChecked()) viewModeWireframe();
2134 else viewModeSurface();
2135 }
2136 else {
2137 LOG(message_group::UI_Warning,Location::NONE,"","No top level geometry to render");
2138 }
2139
2140 updateStatusBar(nullptr);
2141
2142 if (Preferences::inst()->getValue("advanced/enableSoundNotification").toBool() &&
2143 Preferences::inst()->getValue("advanced/timeThresholdOnRenderCompleteSound").toUInt() <= ms.count()/1000)
2144 {
2145 QSound::play(":sounds/complete.wav");
2146 }
2147
2148 renderedEditor = activeEditor;
2149 activeEditor->contentsRendered = true;
2150 compileEnded();
2151 }
2152
2153 #endif /* ENABLE_CGAL */
2154
2155 /**
2156 * Call the mouseselection to determine the id of the clicked-on object.
2157 * Use the generated ID and try to find it within the list of products
2158 * And finally move the cursor to the beginning of the selected object in the editor
2159 */
selectObject(QPoint mouse)2160 void MainWindow::selectObject(QPoint mouse)
2161 {
2162 // selecting without a renderer?!
2163 if (!this->qglview->renderer) {
2164 return;
2165 }
2166
2167 // selecting without select object?!
2168 if (!this->selector) {
2169 return;
2170 }
2171
2172 // Nothing to select
2173 if (!this->root_products) {
2174 return;
2175 }
2176
2177 this->qglview->renderer->prepare(true, false, &this->selector->shaderinfo);
2178
2179 // Update the selector with the right image size
2180 this->selector->reset(this->qglview);
2181
2182 // Select the object at mouse coordinates
2183 int index = this->selector->select(this->qglview->renderer, mouse.x(), mouse.y());
2184 std::deque<const AbstractNode *> path;
2185 const AbstractNode *result = this->root_node->getNodeByID(index, path);
2186
2187 if (result) {
2188 // Create context menu with the backtrace
2189 QMenu tracemenu(this);
2190 std::stringstream ss;
2191 for (const auto *step : path) {
2192 // Skip certain node types
2193 if (step->name() == "root") {
2194 continue;
2195 }
2196
2197 auto location = step->location;
2198 ss.str("");
2199
2200 // Check if the path is contained in a library (using parsersettings.h)
2201 fs::path libpath = get_library_for_path(location.filePath());
2202 if (!libpath.empty()) {
2203 // Display the library (without making the window too wide!)
2204 ss << step->verbose_name() << " (library "
2205 << location.fileName().substr(libpath.string().length() + 1) << ":"
2206 << location.firstLine() << ")";
2207 }
2208 else if (activeEditor->filepath.toStdString() == location.fileName()) {
2209 ss << step->verbose_name() << " (" << location.filePath().filename().string() << ":"
2210 << location.firstLine() << ")";
2211 }
2212 else {
2213 auto relname = boostfs_uncomplete(location.filePath(), fs::path(activeEditor->filepath.toStdString()).parent_path())
2214 .generic_string();
2215 // Set the displayed name relative to the active editor window
2216 ss << step->verbose_name() << " (" << relname << ":" << location.firstLine() << ")";
2217 }
2218
2219 // Prepare the action to be sent
2220 auto action = tracemenu.addAction(QString::fromStdString(ss.str()));
2221 if (editorDock->isVisible()) {
2222 action->setProperty("file", QString::fromStdString(location.fileName()));
2223 action->setProperty("line", location.firstLine());
2224 action->setProperty("column", location.firstColumn());
2225
2226 connect(action, SIGNAL(triggered()), this, SLOT(setCursor()));
2227 }
2228 }
2229
2230 tracemenu.exec(this->qglview->mapToGlobal(mouse));
2231 }
2232 }
2233
2234 /**
2235 * Expects the sender to have properties "file", "line" and "column" defined
2236 */
setCursor()2237 void MainWindow::setCursor()
2238 {
2239 QAction *action = qobject_cast<QAction *>(sender());
2240 if (!action || !action->property("file").isValid() || !action->property("line").isValid() ||
2241 !action->property("column").isValid()) {
2242 return;
2243 }
2244
2245 auto file = action->property("file").toString();
2246 auto line = action->property("line").toInt();
2247 auto column = action->property("column").toInt();
2248
2249 // Unsaved files do have the pwd as current path, therefore we will not open a new
2250 // tab on click
2251 if (!fs::is_directory(fs::path(file.toStdString()))) {
2252 this->tabManager->open(file);
2253 }
2254
2255 // move the cursor, the editor is 0 based whereby location is 1 based
2256 this->activeEditor->setCursorPosition(line - 1, column - 1);
2257 }
2258
2259 /**
2260 * Switch version label and progress widget. When switching to the progress
2261 * widget, the new instance is passed by the caller.
2262 * In case of resetting back to the version label, nullptr will be passed and
2263 * multiple calls can happen. So this method must guard against adding the
2264 * version label multiple times.
2265 *
2266 * @param progressWidget a pointer to the progress widget to show or nullptr in
2267 * case the display should switch back to the version label.
2268 */
updateStatusBar(ProgressWidget * progressWidget)2269 void MainWindow::updateStatusBar(ProgressWidget *progressWidget)
2270 {
2271 auto sb = this->statusBar();
2272 if (progressWidget == nullptr) {
2273 if (this->progresswidget != nullptr) {
2274 sb->removeWidget(this->progresswidget);
2275 delete this->progresswidget;
2276 this->progresswidget = nullptr;
2277 }
2278 if (versionLabel == nullptr) {
2279 versionLabel = new QLabel("OpenSCAD " + QString::fromStdString(openscad_displayversionnumber));
2280 sb->addPermanentWidget(this->versionLabel);
2281 }
2282 } else {
2283 if (this->versionLabel != nullptr) {
2284 sb->removeWidget(this->versionLabel);
2285 delete this->versionLabel;
2286 this->versionLabel = nullptr;
2287 }
2288 sb->addPermanentWidget(progressWidget);
2289 }
2290 }
2291
exceptionCleanup()2292 void MainWindow::exceptionCleanup(){
2293 LOG(message_group::None,Location::NONE,"","Execution aborted");
2294 LOG(message_group::None,Location::NONE,""," ");
2295 GuiLocker::unlock();
2296 if (designActionAutoReload->isChecked()) autoReloadTimer->start();
2297 }
2298
actionDisplayAST()2299 void MainWindow::actionDisplayAST()
2300 {
2301 setCurrentOutput();
2302 auto e = new QTextEdit(this);
2303 e->setAttribute(Qt::WA_DeleteOnClose);
2304 e->setWindowFlags(Qt::Window);
2305 e->setTabStopWidth(tabStopWidth);
2306 e->setWindowTitle("AST Dump");
2307 e->setReadOnly(true);
2308 if (root_module) {
2309 e->setPlainText(QString::fromStdString(root_module->dump("")));
2310 } else {
2311 e->setPlainText("No AST to dump. Please try compiling first...");
2312 }
2313 e->resize(600, 400);
2314 e->show();
2315 clearCurrentOutput();
2316 }
2317
actionDisplayCSGTree()2318 void MainWindow::actionDisplayCSGTree()
2319 {
2320 setCurrentOutput();
2321 auto e = new QTextEdit(this);
2322 e->setAttribute(Qt::WA_DeleteOnClose);
2323 e->setWindowFlags(Qt::Window);
2324 e->setTabStopWidth(tabStopWidth);
2325 e->setWindowTitle("CSG Tree Dump");
2326 e->setReadOnly(true);
2327 if (this->root_node) {
2328 e->setPlainText(QString::fromStdString(this->tree.getString(*this->root_node, " ")));
2329 } else {
2330 e->setPlainText("No CSG to dump. Please try compiling first...");
2331 }
2332 e->resize(600, 400);
2333 e->show();
2334 clearCurrentOutput();
2335 }
2336
actionDisplayCSGProducts()2337 void MainWindow::actionDisplayCSGProducts()
2338 {
2339 std::string NA("N/A");
2340 setCurrentOutput();
2341 auto e = new QTextEdit(this);
2342 e->setAttribute(Qt::WA_DeleteOnClose);
2343 e->setWindowFlags(Qt::Window);
2344 e->setTabStopWidth(tabStopWidth);
2345 e->setWindowTitle("CSG Products Dump");
2346 e->setReadOnly(true);
2347 e->setPlainText(QString("\nCSG before normalization:\n%1\n\n\nCSG after normalization:\n%2\n\n\nCSG rendering chain:\n%3\n\n\nHighlights CSG rendering chain:\n%4\n\n\nBackground CSG rendering chain:\n%5\n")
2348
2349 .arg(QString::fromStdString(this->csgRoot ? this->csgRoot->dump() : NA),
2350 QString::fromStdString(this->normalizedRoot ? this->normalizedRoot->dump() : NA),
2351 QString::fromStdString(this->root_products ? this->root_products->dump() : NA),
2352 QString::fromStdString(this->highlights_products ? this->highlights_products->dump() : NA),
2353 QString::fromStdString(this->background_products ? this->background_products->dump() : NA)));
2354
2355 e->resize(600, 400);
2356 e->show();
2357 clearCurrentOutput();
2358 }
2359
actionCheckValidity()2360 void MainWindow::actionCheckValidity()
2361 {
2362 if (GuiLocker::isLocked()) return;
2363 GuiLocker lock;
2364 #ifdef ENABLE_CGAL
2365 setCurrentOutput();
2366
2367 if (!this->root_geom) {
2368 LOG(message_group::None,Location::NONE,"","Nothing to validate! Try building first (press F6).");
2369 clearCurrentOutput();
2370 return;
2371 }
2372
2373 if (this->root_geom->getDimension() != 3) {
2374 LOG(message_group::None,Location::NONE,"","Current top level object is not a 3D object.");
2375 clearCurrentOutput();
2376 return;
2377 }
2378
2379 bool valid = false;
2380 shared_ptr<const CGAL_Nef_polyhedron> N;
2381 if (auto ps = dynamic_cast<const PolySet *>(this->root_geom.get())) {
2382 N.reset(CGALUtils::createNefPolyhedronFromGeometry(*ps));
2383 }
2384 if (N || (N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(this->root_geom))) {
2385 valid = N->p3 ? const_cast<CGAL_Nef_polyhedron3&>(*N->p3).is_valid() : false;
2386 }
2387 LOG(message_group::None,Location::NONE,"","Valid: %1$6s",(valid ? "yes" : "no"));
2388 clearCurrentOutput();
2389 #endif /* ENABLE_CGAL */
2390 }
2391
2392 //Returns if we can export (true) or not(false) (bool)
2393 //Separated into it's own function for re-use.
canExport(unsigned int dim)2394 bool MainWindow::canExport(unsigned int dim)
2395 {
2396 if (!this->root_geom) {
2397 LOG(message_group::Error,Location::NONE,"","Nothing to export! Try rendering first (press F6)");
2398 clearCurrentOutput();
2399 return false;
2400 }
2401
2402 // editor has changed since last render
2403 if (!activeEditor->contentsRendered) {
2404 auto ret = QMessageBox::warning(this, "Application",
2405 "The current tab has been modified since its last render (F6).\n"
2406 "Do you really want to export the previous content?",
2407 QMessageBox::Yes | QMessageBox::No);
2408 if (ret != QMessageBox::Yes) {
2409 return false;
2410 }
2411 }
2412
2413 // other tab contents most recently rendered
2414 if(renderedEditor != activeEditor) {
2415 auto ret = QMessageBox::warning(this, "Application",
2416 "The rendered data is of different tab.\n"
2417 "Do you really want to export the another tab's content?",
2418 QMessageBox::Yes | QMessageBox::No);
2419 if (ret != QMessageBox::Yes) {
2420 return false;
2421 }
2422 }
2423
2424 if (this->root_geom->getDimension() != dim) {
2425 LOG(message_group::UI_Error,Location::NONE,"","Current top level object is not a %1$dD object.",dim);
2426 clearCurrentOutput();
2427 return false;
2428 }
2429
2430 if (this->root_geom->isEmpty()) {
2431 LOG(message_group::UI_Error,Location::NONE,"","Current top level object is empty.");
2432 clearCurrentOutput();
2433 return false;
2434 }
2435
2436 auto N = dynamic_cast<const CGAL_Nef_polyhedron *>(this->root_geom.get());
2437 if (N && !N->p3->is_simple()) {
2438 LOG(message_group::UI_Warning,Location::NONE,"","Object may not be a valid 2-manifold and may need repair! See https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/STL_Import_and_Export");
2439 }
2440
2441 return true;
2442 }
2443
2444 #ifdef ENABLE_CGAL
actionExport(FileFormat format,const char * type_name,const char * suffix,unsigned int dim)2445 void MainWindow::actionExport(FileFormat format, const char *type_name, const char *suffix, unsigned int dim)
2446 #else
2447 void MainWindow::actionExport(FileFormat, QString, QString, unsigned int, QString)
2448 #endif
2449 {
2450 //Setting filename skips the file selection dialog and uses the path provided instead.
2451
2452 if (GuiLocker::isLocked()) return;
2453 GuiLocker lock;
2454 #ifdef ENABLE_CGAL
2455 setCurrentOutput();
2456
2457 //Return if something is wrong and we can't export.
2458 if (! canExport(dim))
2459 return;
2460 auto title = QString(_("Export %1 File")).arg(type_name);
2461 auto filter = QString(_("%1 Files (*%2)")).arg(type_name, suffix);
2462 auto exportFilename = QFileDialog::getSaveFileName(this, title, exportPath(suffix), filter);
2463 if (exportFilename.isEmpty()) {
2464 clearCurrentOutput();
2465 return;
2466 }
2467 this->export_paths[suffix] = exportFilename;
2468
2469 ExportInfo exportInfo = createExportInfo(format, exportFilename, activeEditor->filepath);
2470 exportFileByName(this->root_geom, exportInfo);
2471
2472 fileExportedMessage(type_name, exportFilename);
2473 clearCurrentOutput();
2474 #endif /* ENABLE_CGAL */
2475 }
2476
actionExportSTL()2477 void MainWindow::actionExportSTL()
2478 {
2479 const auto *s = Settings::Settings::inst();
2480 if (s->get(Settings::Settings::exportUseAsciiSTL).toBool()) {
2481 actionExport(FileFormat::ASCIISTL, "ASCIISTL", ".stl", 3);
2482 }
2483 else {
2484 actionExport(FileFormat::STL, "STL", ".stl", 3);
2485 }
2486 }
2487
actionExport3MF()2488 void MainWindow::actionExport3MF()
2489 {
2490 actionExport(FileFormat::_3MF, "3MF", ".3mf", 3);
2491 }
2492
actionExportOFF()2493 void MainWindow::actionExportOFF()
2494 {
2495 actionExport(FileFormat::OFF, "OFF", ".off", 3);
2496 }
2497
actionExportAMF()2498 void MainWindow::actionExportAMF()
2499 {
2500 actionExport(FileFormat::AMF, "AMF", ".amf", 3);
2501 }
2502
actionExportDXF()2503 void MainWindow::actionExportDXF()
2504 {
2505 actionExport(FileFormat::DXF, "DXF", ".dxf", 2);
2506 }
2507
actionExportSVG()2508 void MainWindow::actionExportSVG()
2509 {
2510 actionExport(FileFormat::SVG, "SVG", ".svg", 2);
2511 }
2512
actionExportPDF()2513 void MainWindow::actionExportPDF()
2514 {
2515 actionExport(FileFormat::PDF, "PDF", ".pdf", 2);
2516 }
2517
actionExportCSG()2518 void MainWindow::actionExportCSG()
2519 {
2520 setCurrentOutput();
2521
2522 if (!this->root_node) {
2523 LOG(message_group::Error,Location::NONE,"","Nothing to export. Please try compiling first.");
2524 clearCurrentOutput();
2525 return;
2526 }
2527 const auto suffix = ".csg";
2528 auto csg_filename = QFileDialog::getSaveFileName(this,
2529 _("Export CSG File"), exportPath(suffix), _("CSG Files (*.csg)"));
2530
2531 if (csg_filename.isEmpty()) {
2532 clearCurrentOutput();
2533 return;
2534 }
2535
2536 std::ofstream fstream(csg_filename.toLocal8Bit());
2537 if (!fstream.is_open()) {
2538 LOG(message_group::None,Location::NONE,"","Can't open file \"%1$s\" for export",csg_filename.toLocal8Bit().constData());
2539 }
2540 else {
2541 fstream << this->tree.getString(*this->root_node, "\t") << "\n";
2542 fstream.close();
2543 fileExportedMessage("CSG", csg_filename);
2544 this->export_paths[suffix] = csg_filename;
2545 }
2546
2547 clearCurrentOutput();
2548 }
2549
actionExportImage()2550 void MainWindow::actionExportImage()
2551 {
2552 // Grab first to make sure dialog box isn't part of the grabbed image
2553 qglview->grabFrame();
2554 const auto suffix = ".png";
2555 auto img_filename = QFileDialog::getSaveFileName(this,
2556 _("Export Image"), exportPath(suffix), _("PNG Files (*.png)"));
2557 if (!img_filename.isEmpty()) {
2558 qglview->save(img_filename.toLocal8Bit().constData());
2559 this->export_paths[suffix] = img_filename;
2560 setCurrentOutput();
2561 fileExportedMessage("PNG", img_filename);
2562 clearCurrentOutput();
2563 }
2564 }
2565
actionCopyViewport()2566 void MainWindow::actionCopyViewport()
2567 {
2568 const auto &image = qglview->grabFrame();
2569 auto clipboard = QApplication::clipboard();
2570 clipboard->setImage(image);
2571 }
2572
actionFlushCaches()2573 void MainWindow::actionFlushCaches()
2574 {
2575 GeometryCache::instance()->clear();
2576 #ifdef ENABLE_CGAL
2577 CGALCache::instance()->clear();
2578 #endif
2579 dxf_dim_cache.clear();
2580 dxf_cross_cache.clear();
2581 ModuleCache::instance()->clear();
2582
2583 setCurrentOutput();
2584 LOG(message_group::None,Location::NONE,"","Caches Flushed");
2585 }
2586
viewModeActionsUncheck()2587 void MainWindow::viewModeActionsUncheck()
2588 {
2589 viewActionPreview->setChecked(false);
2590 #ifdef ENABLE_CGAL
2591 viewActionSurfaces->setChecked(false);
2592 viewActionWireframe->setChecked(false);
2593 #endif
2594 viewActionThrownTogether->setChecked(false);
2595 }
2596
2597 #ifdef ENABLE_OPENCSG
2598
2599 /*!
2600 Go to the OpenCSG view mode.
2601 Falls back to thrown together mode if OpenCSG is not available
2602 */
viewModePreview()2603 void MainWindow::viewModePreview()
2604 {
2605 if (this->qglview->hasOpenCSGSupport()) {
2606 viewModeActionsUncheck();
2607 viewActionPreview->setChecked(true);
2608 this->qglview->setRenderer(this->opencsgRenderer ? (Renderer *)this->opencsgRenderer : (Renderer *)this->thrownTogetherRenderer);
2609 this->qglview->updateColorScheme();
2610 this->qglview->updateGL();
2611 } else {
2612 viewModeThrownTogether();
2613 }
2614 }
2615
2616 #endif /* ENABLE_OPENCSG */
2617
2618 #ifdef ENABLE_CGAL
2619
viewModeSurface()2620 void MainWindow::viewModeSurface()
2621 {
2622 viewModeActionsUncheck();
2623 viewActionSurfaces->setChecked(true);
2624 this->qglview->setShowFaces(true);
2625 this->qglview->setRenderer(this->cgalRenderer);
2626 this->qglview->updateColorScheme();
2627 this->qglview->updateGL();
2628 }
2629
viewModeWireframe()2630 void MainWindow::viewModeWireframe()
2631 {
2632 viewModeActionsUncheck();
2633 viewActionWireframe->setChecked(true);
2634 this->qglview->setShowFaces(false);
2635 this->qglview->setRenderer(this->cgalRenderer);
2636 this->qglview->updateColorScheme();
2637 this->qglview->updateGL();
2638 }
2639
2640 #endif /* ENABLE_CGAL */
2641
viewModeThrownTogether()2642 void MainWindow::viewModeThrownTogether()
2643 {
2644 viewModeActionsUncheck();
2645 viewActionThrownTogether->setChecked(true);
2646 this->qglview->setRenderer(this->thrownTogetherRenderer);
2647 this->qglview->updateColorScheme();
2648 this->qglview->updateGL();
2649 }
2650
viewModeShowEdges()2651 void MainWindow::viewModeShowEdges()
2652 {
2653 QSettingsCached settings;
2654 settings.setValue("view/showEdges",viewActionShowEdges->isChecked());
2655 this->qglview->setShowEdges(viewActionShowEdges->isChecked());
2656 this->qglview->updateGL();
2657 }
2658
viewModeShowAxes()2659 void MainWindow::viewModeShowAxes()
2660 {
2661 bool showaxes = viewActionShowAxes->isChecked();
2662 QSettingsCached settings;
2663 settings.setValue("view/showAxes", showaxes);
2664 this->viewActionShowScaleProportional->setEnabled(showaxes);
2665 this->qglview->setShowAxes(showaxes);
2666 this->qglview->updateGL();
2667 }
2668
viewModeShowCrosshairs()2669 void MainWindow::viewModeShowCrosshairs()
2670 {
2671 QSettingsCached settings;
2672 settings.setValue("view/showCrosshairs",viewActionShowCrosshairs->isChecked());
2673 this->qglview->setShowCrosshairs(viewActionShowCrosshairs->isChecked());
2674 this->qglview->updateGL();
2675 }
2676
viewModeShowScaleProportional()2677 void MainWindow::viewModeShowScaleProportional()
2678 {
2679 QSettingsCached settings;
2680 settings.setValue("view/showScaleProportional",viewActionShowScaleProportional->isChecked());
2681 this->qglview->setShowScaleProportional(viewActionShowScaleProportional->isChecked());
2682 this->qglview->updateGL();
2683 }
2684
viewModeAnimate()2685 void MainWindow::viewModeAnimate()
2686 {
2687 if (viewActionAnimate->isChecked()) {
2688 animate_panel->show();
2689 actionRenderPreview();
2690 updatedAnimFps();
2691 } else {
2692 animate_panel->hide();
2693 animate_timer->stop();
2694 }
2695 }
2696
isEmpty()2697 bool MainWindow::isEmpty()
2698 {
2699 return activeEditor->toPlainText().isEmpty();
2700 }
2701
animateUpdateDocChanged()2702 void MainWindow::animateUpdateDocChanged()
2703 {
2704 auto current_doc = activeEditor->toPlainText();
2705 if (current_doc != last_compiled_doc) {
2706 animateUpdate();
2707 }
2708 }
2709
animateUpdate()2710 void MainWindow::animateUpdate()
2711 {
2712 if (animate_panel->isVisible()) {
2713 bool fps_ok;
2714 double fps = this->e_fps->text().toDouble(&fps_ok);
2715 if (fps_ok && fps <= 0 && !animate_timer->isActive()) {
2716 animate_timer->stop();
2717 animate_timer->setSingleShot(true);
2718 animate_timer->setInterval(50);
2719 animate_timer->start();
2720 }
2721 }
2722 }
2723
viewAngleTop()2724 void MainWindow::viewAngleTop()
2725 {
2726 qglview->cam.object_rot << 90,0,0;
2727 this->qglview->updateGL();
2728 }
2729
viewAngleBottom()2730 void MainWindow::viewAngleBottom()
2731 {
2732 qglview->cam.object_rot << 270,0,0;
2733 this->qglview->updateGL();
2734 }
2735
viewAngleLeft()2736 void MainWindow::viewAngleLeft()
2737 {
2738 qglview->cam.object_rot << 0,0,90;
2739 this->qglview->updateGL();
2740 }
2741
viewAngleRight()2742 void MainWindow::viewAngleRight()
2743 {
2744 qglview->cam.object_rot << 0,0,270;
2745 this->qglview->updateGL();
2746 }
2747
viewAngleFront()2748 void MainWindow::viewAngleFront()
2749 {
2750 qglview->cam.object_rot << 0,0,0;
2751 this->qglview->updateGL();
2752 }
2753
viewAngleBack()2754 void MainWindow::viewAngleBack()
2755 {
2756 qglview->cam.object_rot << 0,0,180;
2757 this->qglview->updateGL();
2758 }
2759
viewAngleDiagonal()2760 void MainWindow::viewAngleDiagonal()
2761 {
2762 qglview->cam.object_rot << 35,0,-25;
2763 this->qglview->updateGL();
2764 }
2765
viewCenter()2766 void MainWindow::viewCenter()
2767 {
2768 qglview->cam.object_trans << 0,0,0;
2769 this->qglview->updateGL();
2770 }
2771
viewPerspective()2772 void MainWindow::viewPerspective()
2773 {
2774 QSettingsCached settings;
2775 settings.setValue("view/orthogonalProjection",false);
2776 viewActionPerspective->setChecked(true);
2777 viewActionOrthogonal->setChecked(false);
2778 this->qglview->setOrthoMode(false);
2779 this->qglview->updateGL();
2780 }
2781
viewOrthogonal()2782 void MainWindow::viewOrthogonal()
2783 {
2784 QSettingsCached settings;
2785 settings.setValue("view/orthogonalProjection",true);
2786 viewActionPerspective->setChecked(false);
2787 viewActionOrthogonal->setChecked(true);
2788 this->qglview->setOrthoMode(true);
2789 this->qglview->updateGL();
2790 }
2791
viewTogglePerspective()2792 void MainWindow::viewTogglePerspective()
2793 {
2794 QSettingsCached settings;
2795 if (settings.value("view/orthogonalProjection").toBool()) {
2796 viewPerspective();
2797 } else {
2798 viewOrthogonal();
2799 }
2800 }
viewResetView()2801 void MainWindow::viewResetView()
2802 {
2803 this->qglview->resetView();
2804 this->qglview->updateGL();
2805 }
2806
viewAll()2807 void MainWindow::viewAll()
2808 {
2809 this->qglview->viewAll();
2810 this->qglview->updateGL();
2811 }
2812
on_editorDock_visibilityChanged(bool)2813 void MainWindow::on_editorDock_visibilityChanged(bool)
2814 {
2815 changedTopLevelEditor(editorDock->isFloating());
2816 tabToolBar->setVisible((tabCount > 1) && editorDock->isVisible());
2817
2818 if (editorDock->isVisible()) viewerToolBar->removeAction(this->fileActionExportSTL);
2819 else{
2820 QAction *beforeAction = viewerToolBar->actions().at(2);
2821 viewerToolBar->insertAction(beforeAction, this->fileActionExportSTL);
2822 }
2823
2824 }
2825
on_consoleDock_visibilityChanged(bool)2826 void MainWindow::on_consoleDock_visibilityChanged(bool)
2827 {
2828 changedTopLevelConsole(consoleDock->isFloating());
2829 }
2830
on_parameterDock_visibilityChanged(bool)2831 void MainWindow::on_parameterDock_visibilityChanged(bool)
2832 {
2833 parameterTopLevelChanged(parameterDock->isFloating());
2834 }
2835
on_errorLogDock_visibilityChanged(bool)2836 void MainWindow::on_errorLogDock_visibilityChanged(bool)
2837 {
2838 errorLogTopLevelChanged(parameterDock->isFloating());
2839 }
2840
changedTopLevelEditor(bool topLevel)2841 void MainWindow::changedTopLevelEditor(bool topLevel)
2842 {
2843 setDockWidgetTitle(editorDock, QString(_("Editor")), topLevel);
2844 }
2845
editorTopLevelChanged(bool topLevel)2846 void MainWindow::editorTopLevelChanged(bool topLevel)
2847 {
2848 setDockWidgetTitle(editorDock, QString(_("Editor")), topLevel);
2849 if(topLevel)
2850 {
2851 this->removeToolBar(tabToolBar);
2852 ((QVBoxLayout *)editorDockContents->layout())->insertWidget(0, tabToolBar);
2853 }
2854 else
2855 {
2856 editorDockContents->layout()->removeWidget(tabToolBar);
2857 this->addToolBar(tabToolBar);
2858 }
2859 tabToolBar->setVisible((tabCount > 1) && editorDock->isVisible());
2860 }
2861
changedTopLevelConsole(bool topLevel)2862 void MainWindow::changedTopLevelConsole(bool topLevel)
2863 {
2864 setDockWidgetTitle(consoleDock, QString(_("Console")), topLevel);
2865 }
2866
consoleTopLevelChanged(bool topLevel)2867 void MainWindow::consoleTopLevelChanged(bool topLevel)
2868 {
2869 setDockWidgetTitle(consoleDock, QString(_("Console")), topLevel);
2870
2871 Qt::WindowFlags flags = (consoleDock->windowFlags() & ~Qt::WindowType_Mask) | Qt::Window;
2872 if(topLevel)
2873 {
2874 consoleDock->setWindowFlags(flags);
2875 consoleDock->show();
2876 }
2877 }
2878
parameterTopLevelChanged(bool topLevel)2879 void MainWindow::parameterTopLevelChanged(bool topLevel)
2880 {
2881 setDockWidgetTitle(parameterDock, QString(_("Customizer")), topLevel);
2882 }
2883
changedTopLevelErrorLog(bool topLevel)2884 void MainWindow::changedTopLevelErrorLog(bool topLevel)
2885 {
2886 setDockWidgetTitle(errorLogDock, QString(_("Error-Log")), topLevel);
2887 }
2888
errorLogTopLevelChanged(bool topLevel)2889 void MainWindow::errorLogTopLevelChanged(bool topLevel)
2890 {
2891 setDockWidgetTitle(errorLogDock, QString(_("Error-Log")), topLevel);
2892
2893 Qt::WindowFlags flags = (errorLogDock->windowFlags() & ~Qt::WindowType_Mask) | Qt::Window;
2894 if(topLevel)
2895 {
2896 errorLogDock->setWindowFlags(flags);
2897 errorLogDock->show();
2898 }
2899 }
2900
setDockWidgetTitle(QDockWidget * dockWidget,QString prefix,bool topLevel)2901 void MainWindow::setDockWidgetTitle(QDockWidget *dockWidget, QString prefix, bool topLevel)
2902 {
2903 QString title(prefix);
2904 if (topLevel) {
2905 const QFileInfo fileInfo(activeEditor->filepath);
2906 QString fname = _("Untitled.scad");
2907 if(!fileInfo.fileName().isEmpty())
2908 fname = fileInfo.fileName();
2909 title += " (" + fname.replace("&", "&&") + ")";
2910 }
2911 dockWidget->setWindowTitle(title);
2912 }
2913
hideEditorToolbar()2914 void MainWindow::hideEditorToolbar()
2915 {
2916 QSettingsCached settings;
2917 bool shouldHide = viewActionHideEditorToolBar->isChecked();
2918 settings.setValue("view/hideEditorToolbar", shouldHide);
2919
2920 if (shouldHide) {
2921 editortoolbar->hide();
2922 } else {
2923 editortoolbar->show();
2924 }
2925 }
2926
hide3DViewToolbar()2927 void MainWindow::hide3DViewToolbar()
2928 {
2929 QSettingsCached settings;
2930 bool shouldHide = viewActionHide3DViewToolBar->isChecked();
2931 settings.setValue("view/hide3DViewToolbar", shouldHide);
2932
2933 if (shouldHide) {
2934 viewerToolBar->hide();
2935 } else {
2936 viewerToolBar->show();
2937 }
2938 }
2939
showLink(const QString link)2940 void MainWindow::showLink(const QString link)
2941 {
2942 if (link == "#console") {
2943 showConsole();
2944 } else if (link == "#errorlog") {
2945 showErrorLog();
2946 }
2947 }
2948
showEditor()2949 void MainWindow::showEditor()
2950 {
2951 windowActionHideEditor->setChecked(false);
2952 hideEditor();
2953 editorDock->raise();
2954 tabManager->setFocus();
2955 }
2956
hideEditor()2957 void MainWindow::hideEditor()
2958 {
2959 auto e = (ScintillaEditor *) this->activeEditor;
2960 if (windowActionHideEditor->isChecked()) {
2961 // Workaround manually disabling interactions with editor by setting it
2962 // to read-only when not being shown. This is an upstream bug from Qt
2963 // (tracking ticket: https://bugreports.qt.io/browse/QTBUG-82939) and
2964 // may eventually get resolved at which point this bit and the stuff in
2965 // the else should be removed. Currently known to affect 5.14.1 and 5.15.0
2966 e->qsci->setReadOnly(true);
2967 e->setupAutoComplete(true);
2968 editorDock->close();
2969 } else {
2970 e->qsci->setReadOnly(false);
2971 e->setupAutoComplete(false);
2972 editorDock->show();
2973 }
2974 }
2975
showConsole()2976 void MainWindow::showConsole()
2977 {
2978 windowActionHideConsole->setChecked(false);
2979 frameCompileResult->hide();
2980 consoleDock->show();
2981 consoleDock->raise();
2982 console->setFocus();
2983 }
2984
hideConsole()2985 void MainWindow::hideConsole()
2986 {
2987 if (windowActionHideConsole->isChecked()) {
2988 consoleDock->hide();
2989 } else {
2990 consoleDock->show();
2991 }
2992 }
2993
showErrorLog()2994 void MainWindow::showErrorLog()
2995 {
2996 windowActionHideErrorLog->setChecked(false);
2997 frameCompileResult->hide();
2998 errorLogDock->show();
2999 errorLogDock->raise();
3000 errorLogWidget->logTable->setFocus();
3001 }
3002
hideErrorLog()3003 void MainWindow::hideErrorLog()
3004 {
3005 if (windowActionHideErrorLog->isChecked()) {
3006 errorLogDock->hide();
3007 } else {
3008 errorLogDock->show();
3009 }
3010 }
3011
showParameters()3012 void MainWindow::showParameters()
3013 {
3014 windowActionHideCustomizer->setChecked(false);
3015 parameterDock->show();
3016 parameterDock->raise();
3017 parameterWidget->scrollArea->setFocus();
3018 }
3019
hideParameters()3020 void MainWindow::hideParameters()
3021 {
3022 if (windowActionHideCustomizer->isChecked()) {
3023 parameterDock->hide();
3024 } else {
3025 parameterDock->show();
3026 }
3027 }
3028
on_windowActionSelectEditor_triggered()3029 void MainWindow::on_windowActionSelectEditor_triggered()
3030 {
3031 showEditor();
3032 }
3033
on_windowActionSelectConsole_triggered()3034 void MainWindow::on_windowActionSelectConsole_triggered()
3035 {
3036 showConsole();
3037 }
3038
on_windowActionSelectErrorLog_triggered()3039 void MainWindow::on_windowActionSelectErrorLog_triggered()
3040 {
3041 showErrorLog();
3042 }
3043
on_windowActionSelectCustomizer_triggered()3044 void MainWindow::on_windowActionSelectCustomizer_triggered()
3045 {
3046 showParameters();
3047 }
3048
on_windowActionNextWindow_triggered()3049 void MainWindow::on_windowActionNextWindow_triggered()
3050 {
3051 activateWindow(1);
3052 }
3053
on_windowActionPreviousWindow_triggered()3054 void MainWindow::on_windowActionPreviousWindow_triggered()
3055 {
3056 activateWindow(-1);
3057 }
3058
on_editActionInsertTemplate_triggered()3059 void MainWindow::on_editActionInsertTemplate_triggered()
3060 {
3061 activeEditor->displayTemplates();
3062 }
3063
activateWindow(int offset)3064 void MainWindow::activateWindow(int offset)
3065 {
3066 const std::array<DockFocus, 4> docks = {{
3067 { editorDock, &MainWindow::on_windowActionSelectEditor_triggered },
3068 { consoleDock, &MainWindow::on_windowActionSelectConsole_triggered },
3069 { errorLogDock, &MainWindow::on_windowActionSelectErrorLog_triggered },
3070 { parameterDock, &MainWindow::on_windowActionSelectCustomizer_triggered },
3071 }};
3072
3073 const int cnt = docks.size();
3074 const auto focusWidget = QApplication::focusWidget();
3075 for (auto widget = focusWidget;widget != nullptr;widget = widget->parentWidget()) {
3076 for (int idx = 0;idx < cnt;++idx) {
3077 if (widget == docks.at(idx).widget) {
3078 for (int o = 1;o < cnt;++o) {
3079 const int target = (cnt + idx + o * offset) % cnt;
3080 const auto dock = docks.at(target);
3081 if (dock.widget->isVisible()) {
3082 dock.focus(this);
3083 return;
3084 }
3085 }
3086 }
3087 }
3088 }
3089 }
3090
dragEnterEvent(QDragEnterEvent * event)3091 void MainWindow::dragEnterEvent(QDragEnterEvent *event)
3092 {
3093 if (event->mimeData()->hasUrls()) {
3094 event->acceptProposedAction();
3095 }
3096 }
3097
dropEvent(QDropEvent * event)3098 void MainWindow::dropEvent(QDropEvent *event)
3099 {
3100 setCurrentOutput();
3101 const QList<QUrl> urls = event->mimeData()->urls();
3102 for (int i = 0; i < urls.size(); ++i) {
3103 handleFileDrop(urls[i]);
3104 }
3105 clearCurrentOutput();
3106 }
3107
handleFileDrop(const QUrl & url)3108 void MainWindow::handleFileDrop(const QUrl& url)
3109 {
3110 if (url.scheme() != "file") return;
3111 const auto fileName = url.toLocalFile();
3112 const auto fileInfo = QFileInfo{fileName};
3113 const auto suffix = fileInfo.suffix().toLower();
3114 const auto cmd = knownFileExtensions[suffix];
3115 if (cmd.isEmpty()) {
3116 tabManager->open(fileName);
3117 } else {
3118 activeEditor->insert(cmd.arg(fileName));
3119 }
3120 }
3121
helpAbout()3122 void MainWindow::helpAbout()
3123 {
3124 qApp->setWindowIcon(QApplication::windowIcon());
3125 auto dialog = new AboutDialog(this);
3126 dialog->exec();
3127 dialog->deleteLater();
3128 }
3129
helpHomepage()3130 void MainWindow::helpHomepage()
3131 {
3132 UIUtils::openHomepageURL();
3133 }
3134
helpManual()3135 void MainWindow::helpManual()
3136 {
3137 UIUtils::openUserManualURL();
3138 }
3139
helpCheatSheet()3140 void MainWindow::helpCheatSheet()
3141 {
3142 UIUtils::openCheatSheetURL();
3143 }
3144
helpLibrary()3145 void MainWindow::helpLibrary()
3146 {
3147 if (!this->library_info_dialog) {
3148 QString rendererInfo(qglview->getRendererInfo().c_str());
3149 auto dialog = new LibraryInfoDialog(rendererInfo);
3150 this->library_info_dialog = dialog;
3151 }
3152 this->library_info_dialog->show();
3153 }
3154
helpFontInfo()3155 void MainWindow::helpFontInfo()
3156 {
3157 if (!this->font_list_dialog) {
3158 auto dialog = new FontListDialog();
3159 this->font_list_dialog = dialog;
3160 }
3161 this->font_list_dialog->update_font_list();
3162 this->font_list_dialog->show();
3163 }
3164
closeEvent(QCloseEvent * event)3165 void MainWindow::closeEvent(QCloseEvent *event)
3166 {
3167 if (tabManager->shouldClose()) {
3168 // Disable invokeMethod calls for consoleOutput during shutdown,
3169 // otherwise will segfault if echos are in progress.
3170 hideCurrentOutput();
3171
3172 QSettingsCached settings;
3173 settings.setValue("window/size", size());
3174 settings.setValue("window/position", pos());
3175 settings.setValue("window/state", saveState());
3176 if (this->tempFile) {
3177 delete this->tempFile;
3178 this->tempFile = nullptr;
3179 }
3180 for (auto dock : findChildren<Dock *>()) {
3181 dock->disableSettingsUpdate();
3182 }
3183 event->accept();
3184 } else {
3185 event->ignore();
3186 }
3187 }
3188
preferences()3189 void MainWindow::preferences()
3190 {
3191 Preferences::inst()->show();
3192 Preferences::inst()->activateWindow();
3193 Preferences::inst()->raise();
3194 }
3195
setColorScheme(const QString & scheme)3196 void MainWindow::setColorScheme(const QString &scheme)
3197 {
3198 RenderSettings::inst()->colorscheme = scheme.toStdString();
3199 this->qglview->setColorScheme(scheme.toStdString());
3200 this->qglview->updateGL();
3201 }
3202
setFont(const QString & family,uint size)3203 void MainWindow::setFont(const QString &family, uint size)
3204 {
3205 QFont font;
3206 if (!family.isEmpty()) font.setFamily(family);
3207 else font.setFixedPitch(true);
3208 if (size > 0) font.setPointSize(size);
3209 font.setStyleHint(QFont::TypeWriter);
3210 activeEditor->setFont(font);
3211 }
3212
quit()3213 void MainWindow::quit()
3214 {
3215 QCloseEvent ev;
3216 QApplication::sendEvent(QApplication::instance(), &ev);
3217 if (ev.isAccepted()) QApplication::instance()->quit();
3218 // FIXME: Cancel any CGAL calculations
3219 #ifdef Q_OS_MAC
3220 CocoaUtils::endApplication();
3221 #endif
3222 }
3223
consoleOutput(const Message & msgObj,void * userdata)3224 void MainWindow::consoleOutput(const Message &msgObj, void *userdata)
3225 {
3226 // Invoke the method in the main thread in case the output
3227 // originates in a worker thread.
3228 auto thisp = static_cast<MainWindow*>(userdata);
3229 QMetaObject::invokeMethod(thisp, "consoleOutput", Q_ARG(Message, msgObj));
3230 }
3231
consoleOutput(const Message & msgObj)3232 void MainWindow::consoleOutput(const Message &msgObj)
3233 {
3234 this->console->addMessage(msgObj);
3235 if (msgObj.group==message_group::Warning || msgObj.group==message_group::Deprecated) {
3236 ++this->compileWarnings;
3237 } else if (msgObj.group==message_group::Error) {
3238 ++this->compileErrors;
3239 }
3240 // FIXME: scad parsing/evaluation should be done on separate thread so as not to block the gui.
3241 // Then processEvents should no longer be needed here.
3242 this->processEvents();
3243 if (consoleUpdater && !consoleUpdater->isActive()) {
3244 consoleUpdater->start(50); // Limit console updates to 20 FPS
3245 }
3246 }
3247
consoleOutputRaw(const QString & html)3248 void MainWindow::consoleOutputRaw(const QString& html)
3249 {
3250 this->console->addHtml(html);
3251 this->processEvents();
3252 }
3253
errorLogOutput(const Message & log_msg,void * userdata)3254 void MainWindow::errorLogOutput(const Message &log_msg, void *userdata)
3255 {
3256 auto thisp = static_cast<MainWindow*>(userdata);
3257 QMetaObject::invokeMethod(thisp, "errorLogOutput", Q_ARG(Message, log_msg));
3258 }
3259
errorLogOutput(const Message & log_msg)3260 void MainWindow::errorLogOutput(const Message &log_msg)
3261 {
3262 this->errorLogWidget->toErrorLog(log_msg);
3263 }
3264
setCurrentOutput()3265 void MainWindow::setCurrentOutput()
3266 {
3267 set_output_handler(&MainWindow::consoleOutput, &MainWindow::errorLogOutput, this);
3268 }
3269
hideCurrentOutput()3270 void MainWindow::hideCurrentOutput()
3271 {
3272 set_output_handler(&MainWindow::noOutputConsole, &MainWindow::noOutputErrorLog, this);
3273 }
3274
clearCurrentOutput()3275 void MainWindow::clearCurrentOutput()
3276 {
3277 set_output_handler(nullptr, nullptr, nullptr);
3278 }
3279
openCSGSettingsChanged()3280 void MainWindow::openCSGSettingsChanged()
3281 {
3282 #ifdef ENABLE_OPENCSG
3283 OpenCSG::setOption(OpenCSG::AlgorithmSetting, Preferences::inst()->getValue("advanced/forceGoldfeather").toBool() ?
3284 OpenCSG::Goldfeather : OpenCSG::Automatic);
3285 #endif
3286 }
3287
processEvents()3288 void MainWindow::processEvents()
3289 {
3290 if (this->procevents) QApplication::processEvents();
3291 }
3292
exportPath(const char * suffix)3293 QString MainWindow::exportPath(const char *suffix) {
3294 QString path;
3295 auto path_it = this->export_paths.find(suffix);
3296 if(path_it != export_paths.end())
3297 {
3298 path = QFileInfo(path_it->second).absolutePath() + QString("/");
3299 if(activeEditor->filepath.isEmpty())
3300 path += QString(_("Untitled")) + suffix;
3301 else
3302 path += QFileInfo(activeEditor->filepath).completeBaseName() + suffix;
3303 }
3304 else
3305 {
3306 if(activeEditor->filepath.isEmpty())
3307 path = QString(PlatformUtils::userDocumentsPath().c_str()) + QString("/") + QString(_("Untitled")) + suffix;
3308 else {
3309 auto info = QFileInfo(activeEditor->filepath);
3310 path = info.absolutePath() + QString("/") + info.completeBaseName() + suffix;
3311 }
3312 }
3313 return path;
3314 }
3315
jumpToLine(int line,int col)3316 void MainWindow::jumpToLine(int line,int col)
3317 {
3318 this->activeEditor->setCursorPosition(line, col);
3319 }
3320