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("&nbsp;")) {
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 &nbsp;<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