7 const QString Window::RECENT_FILE_KEY = "recentFiles";
8 const QString Window::INVERT_ZOOM_KEY = "invertZoom";
9 const QString Window::AUTORELOAD_KEY = "autoreload";
10 const QString Window::DRAW_AXES_KEY = "drawAxes";
11 const QString Window::PROJECTION_KEY = "projection";
12 const QString Window::DRAW_MODE_KEY = "drawMode";
13 const QString Window::WINDOW_GEOM_KEY = "windowGeometry";
15 Window::Window(QWidget *parent) :
17 open_action(new QAction("Open", this)),
18 about_action(new QAction("About", this)),
19 quit_action(new QAction("Quit", this)),
20 perspective_action(new QAction("Perspective", this)),
21 orthographic_action(new QAction("Orthographic", this)),
22 shaded_action(new QAction("Shaded", this)),
23 wireframe_action(new QAction("Wireframe", this)),
24 surfaceangle_action(new QAction("Surface Angle", this)),
25 axes_action(new QAction("Draw Axes", this)),
26 invert_zoom_action(new QAction("Invert Zoom", this)),
27 reload_action(new QAction("Reload", this)),
28 autoreload_action(new QAction("Autoreload", this)),
29 save_screenshot_action(new QAction("Save Screenshot", this)),
30 hide_menuBar_action(new QAction("Hide Menu Bar", this)),
31 recent_files(new QMenu("Open recent", this)),
32 recent_files_group(new QActionGroup(this)),
33 recent_files_clear_action(new QAction("Clear recent files", this)),
34 watcher(new QFileSystemWatcher(this))
37 setWindowTitle("fstl");
40 QSurfaceFormat format;
41 format.setDepthBufferSize(24);
42 format.setStencilBufferSize(8);
43 format.setVersion(2, 1);
44 format.setProfile(QSurfaceFormat::CoreProfile);
46 QSurfaceFormat::setDefaultFormat(format);
48 canvas = new Canvas(format, this);
49 setCentralWidget(canvas);
51 QObject::connect(watcher, &QFileSystemWatcher::fileChanged,
52 this, &Window::on_watched_change);
54 open_action->setShortcut(QKeySequence::Open);
55 QObject::connect(open_action, &QAction::triggered,
56 this, &Window::on_open);
57 this->addAction(open_action);
59 quit_action->setShortcut(QKeySequence::Quit);
60 QObject::connect(quit_action, &QAction::triggered,
61 this, &Window::close);
62 this->addAction(quit_action);
64 autoreload_action->setCheckable(true);
65 QObject::connect(autoreload_action, &QAction::triggered,
66 this, &Window::on_autoreload_triggered);
68 reload_action->setShortcut(QKeySequence::Refresh);
69 reload_action->setEnabled(false);
70 QObject::connect(reload_action, &QAction::triggered,
71 this, &Window::on_reload);
73 QObject::connect(about_action, &QAction::triggered,
74 this, &Window::on_about);
76 QObject::connect(recent_files_clear_action, &QAction::triggered,
77 this, &Window::on_clear_recent);
78 QObject::connect(recent_files_group, &QActionGroup::triggered,
79 this, &Window::on_load_recent);
81 save_screenshot_action->setCheckable(false);
82 QObject::connect(save_screenshot_action, &QAction::triggered,
83 this, &Window::on_save_screenshot);
85 rebuild_recent_files();
87 auto file_menu = menuBar()->addMenu("File");
88 file_menu->addAction(open_action);
89 file_menu->addMenu(recent_files);
90 file_menu->addSeparator();
91 file_menu->addAction(reload_action);
92 file_menu->addAction(autoreload_action);
93 file_menu->addAction(save_screenshot_action);
94 file_menu->addAction(quit_action);
96 auto view_menu = menuBar()->addMenu("View");
97 auto projection_menu = view_menu->addMenu("Projection");
98 projection_menu->addAction(perspective_action);
99 projection_menu->addAction(orthographic_action);
100 auto projections = new QActionGroup(projection_menu);
101 for (auto p : {perspective_action, orthographic_action})
103 projections->addAction(p);
104 p->setCheckable(true);
106 projections->setExclusive(true);
107 QObject::connect(projections, &QActionGroup::triggered,
108 this, &Window::on_projection);
110 auto draw_menu = view_menu->addMenu("Draw Mode");
111 draw_menu->addAction(shaded_action);
112 draw_menu->addAction(wireframe_action);
113 draw_menu->addAction(surfaceangle_action);
114 auto drawModes = new QActionGroup(draw_menu);
115 for (auto p : {shaded_action, wireframe_action, surfaceangle_action})
117 drawModes->addAction(p);
118 p->setCheckable(true);
120 drawModes->setExclusive(true);
121 QObject::connect(drawModes, &QActionGroup::triggered,
122 this, &Window::on_drawMode);
123 view_menu->addAction(axes_action);
124 axes_action->setCheckable(true);
125 QObject::connect(axes_action, &QAction::triggered,
126 this, &Window::on_drawAxes);
128 view_menu->addAction(invert_zoom_action);
129 invert_zoom_action->setCheckable(true);
130 QObject::connect(invert_zoom_action, &QAction::triggered,
131 this, &Window::on_invertZoom);
133 view_menu->addAction(hide_menuBar_action);
134 hide_menuBar_action->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C);
135 hide_menuBar_action->setCheckable(true);
136 QObject::connect(hide_menuBar_action, &QAction::toggled,
137 this, &Window::on_hide_menuBar);
138 this->addAction(hide_menuBar_action);
140 auto help_menu = menuBar()->addMenu("Help");
141 help_menu->addAction(about_action);
143 load_persist_settings();
146 void Window::load_persist_settings(){
148 bool invert_zoom = settings.value(INVERT_ZOOM_KEY, false).toBool();
149 canvas->invert_zoom(invert_zoom);
150 invert_zoom_action->setChecked(invert_zoom);
152 autoreload_action->setChecked(settings.value(AUTORELOAD_KEY, true).toBool());
154 bool draw_axes = settings.value(DRAW_AXES_KEY, false).toBool();
155 canvas->draw_axes(draw_axes);
156 axes_action->setChecked(draw_axes);
158 QString projection = settings.value(PROJECTION_KEY, "perspective").toString();
159 if(projection == "perspective"){
160 canvas->view_perspective(Canvas::P_PERSPECTIVE, false);
161 perspective_action->setChecked(true);
163 canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, false);
164 orthographic_action->setChecked(true);
167 DrawMode draw_mode = (DrawMode)settings.value(DRAW_MODE_KEY, DRAWMODECOUNT).toInt();
169 if(draw_mode >= DRAWMODECOUNT)
173 canvas->set_drawMode(draw_mode);
174 QAction* (dm_acts[]) = {shaded_action, wireframe_action, surfaceangle_action};
175 dm_acts[draw_mode]->setChecked(true);
178 restoreGeometry(settings.value(WINDOW_GEOM_KEY).toByteArray());
181 void Window::on_open()
183 QString filename = QFileDialog::getOpenFileName(
184 this, "Load .stl file", QString(), "STL files (*.stl, *.STL)");
185 if (!filename.isNull())
191 void Window::on_about()
193 QMessageBox::about(this, "",
194 "<p align=\"center\"><b>fstl</b><br>" FSTL_VERSION "</p>"
195 "<p>A fast viewer for <code>.stl</code> files.<br>"
196 "<a href=\"https://github.com/fstl-app/fstl\""
197 " style=\"color: #93a1a1;\">https://github.com/fstl-app/fstl</a></p>"
198 "<p>© 2014-2022 Matthew Keeter<br>"
199 "<a href=\"mailto:matt.j.keeter@gmail.com\""
200 " style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
203 void Window::on_bad_stl()
205 QMessageBox::critical(this, "Error",
207 "This <code>.stl</code> file is invalid or corrupted.<br>"
208 "Please export it from the original source, verify, and retry.");
211 void Window::on_empty_mesh()
213 QMessageBox::critical(this, "Error",
215 "This file is syntactically correct<br>but contains no triangles.");
218 void Window::on_missing_file()
220 QMessageBox::critical(this, "Error",
222 "The target file is missing.<br>");
225 void Window::enable_open()
227 open_action->setEnabled(true);
230 void Window::disable_open()
232 open_action->setEnabled(false);
235 void Window::set_watched(const QString& filename)
237 const auto files = watcher->files();
240 watcher->removePaths(watcher->files());
242 watcher->addPath(filename);
245 auto recent = settings.value(RECENT_FILE_KEY).toStringList();
246 const auto f = QFileInfo(filename).absoluteFilePath();
249 while (recent.size() > MAX_RECENT_FILES)
253 settings.setValue(RECENT_FILE_KEY, recent);
254 rebuild_recent_files();
257 void Window::on_projection(QAction* proj)
259 if (proj == perspective_action)
261 canvas->view_perspective(Canvas::P_PERSPECTIVE, true);
262 QSettings().setValue(PROJECTION_KEY, "perspective");
266 canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, true);
267 QSettings().setValue(PROJECTION_KEY, "orthographic");
271 void Window::on_drawMode(QAction* act)
274 if (act == shaded_action)
278 else if (act == wireframe_action)
286 canvas->set_drawMode(mode);
287 QSettings().setValue(DRAW_MODE_KEY, mode);
290 void Window::on_drawAxes(bool d)
292 canvas->draw_axes(d);
293 QSettings().setValue(DRAW_AXES_KEY, d);
296 void Window::on_invertZoom(bool d)
298 canvas->invert_zoom(d);
299 QSettings().setValue(INVERT_ZOOM_KEY, d);
302 void Window::on_watched_change(const QString& filename)
304 if (autoreload_action->isChecked())
306 load_stl(filename, true);
310 void Window::on_autoreload_triggered(bool b)
316 QSettings().setValue(AUTORELOAD_KEY, b);
319 void Window::on_clear_recent()
322 settings.setValue(RECENT_FILE_KEY, QStringList());
323 rebuild_recent_files();
326 void Window::on_load_recent(QAction* a)
328 load_stl(a->data().toString());
331 void Window::on_loaded(const QString& filename)
333 current_file = filename;
336 void Window::on_save_screenshot()
338 const auto image = canvas->grabFramebuffer();
339 auto file_name = QFileDialog::getSaveFileName(
341 tr("Save Screenshot Image"),
342 QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(),
343 "Images (*.png *.jpg)");
345 auto get_file_extension = [](const std::string& file_name) -> std::string
347 const auto location = std::find(file_name.rbegin(), file_name.rend(), '.');
348 if (location == file_name.rend())
353 const auto index = std::distance(file_name.rbegin(), location);
354 return file_name.substr(file_name.size() - index);
357 const auto extension = get_file_extension(file_name.toStdString());
358 if(extension.empty() || (extension != "png" && extension != "jpg"))
360 file_name.append(".png");
363 const auto save_ok = image.save(file_name);
366 QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image."));
370 void Window::on_hide_menuBar()
372 menuBar()->setVisible(!hide_menuBar_action->isChecked());
375 void Window::rebuild_recent_files()
378 QStringList files = settings.value(RECENT_FILE_KEY).toStringList();
380 const auto actions = recent_files_group->actions();
381 for (auto a : actions)
383 recent_files_group->removeAction(a);
385 recent_files->clear();
389 const auto a = new QAction(f, recent_files);
391 recent_files_group->addAction(a);
392 recent_files->addAction(a);
394 if (files.size() == 0)
396 auto a = new QAction("No recent files", recent_files);
397 recent_files->addAction(a);
398 a->setEnabled(false);
400 recent_files->addSeparator();
401 recent_files->addAction(recent_files_clear_action);
404 void Window::on_reload()
406 auto fs = watcher->files();
409 load_stl(fs[0], true);
413 bool Window::load_stl(const QString& filename, bool is_reload)
415 if (!open_action->isEnabled()) return false;
417 canvas->set_status("Loading " + filename);
419 Loader* loader = new Loader(this, filename, is_reload);
420 connect(loader, &Loader::started,
421 this, &Window::disable_open);
423 connect(loader, &Loader::got_mesh,
424 canvas, &Canvas::load_mesh);
425 connect(loader, &Loader::error_bad_stl,
426 this, &Window::on_bad_stl);
427 connect(loader, &Loader::error_empty_mesh,
428 this, &Window::on_empty_mesh);
429 connect(loader, &Loader::error_missing_file,
430 this, &Window::on_missing_file);
432 connect(loader, &Loader::finished,
433 loader, &Loader::deleteLater);
434 connect(loader, &Loader::finished,
435 this, &Window::enable_open);
436 connect(loader, &Loader::finished,
437 canvas, &Canvas::clear_status);
439 if (filename[0] != ':')
441 connect(loader, &Loader::loaded_file,
442 this, &Window::setWindowTitle);
443 connect(loader, &Loader::loaded_file,
444 this, &Window::set_watched);
445 connect(loader, &Loader::loaded_file,
446 this, &Window::on_loaded);
447 reload_action->setEnabled(true);
454 void Window::dragEnterEvent(QDragEnterEvent *event)
456 if (event->mimeData()->hasUrls())
458 auto urls = event->mimeData()->urls();
459 if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
460 event->acceptProposedAction();
464 void Window::dropEvent(QDropEvent *event)
466 load_stl(event->mimeData()->urls().front().toLocalFile());
469 void Window::resizeEvent(QResizeEvent *event)
471 QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry());
472 QWidget::resizeEvent(event);
475 void Window::moveEvent(QMoveEvent *event)
477 QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry());
478 QWidget::moveEvent(event);
481 void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value)
484 int end = list.size() - 1;
486 while (start <= end){
487 int mid = (start+end)/2;
488 if (list[mid] == value) {
491 int compare = collator.compare(value, list[mid]);
501 list.insert(index, value);
504 void Window::build_folder_file_list()
506 QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath();
507 if (!lookup_folder_files.isEmpty())
509 if (current_folder_path == lookup_folder) {
513 lookup_folder_files.clear();
515 lookup_folder = current_folder_path;
518 collator.setNumericMode(true);
520 QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden);
521 while (dirIterator.hasNext()) {
524 QString name = dirIterator.fileName();
525 sorted_insert(lookup_folder_files, collator, name);
529 QPair<QString, QString> Window::get_file_neighbors()
531 if (current_file.isEmpty()) {
532 return QPair<QString, QString>(QString(), QString());
535 build_folder_file_list();
537 QFileInfo fileInfo(current_file);
539 QString current_dir = fileInfo.absoluteDir().absolutePath();
540 QString current_name = fileInfo.fileName();
542 QString prev = QString();
543 QString next = QString();
545 QListIterator<QString> fileIterator(lookup_folder_files);
546 while (fileIterator.hasNext()) {
547 QString name = fileIterator.next();
549 if (name == current_name) {
550 if (fileIterator.hasNext()) {
551 next = current_dir + QDir::separator() + fileIterator.next();
559 if (!prev.isEmpty()) {
560 prev.prepend(QDir::separator());
561 prev.prepend(current_dir);
564 return QPair<QString, QString>(prev, next);
567 bool Window::load_prev(void)
569 QPair<QString, QString> neighbors = get_file_neighbors();
570 if (neighbors.first.isEmpty()) {
574 return load_stl(neighbors.first);
577 bool Window::load_next(void)
579 QPair<QString, QString> neighbors = get_file_neighbors();
580 if (neighbors.second.isEmpty()) {
584 return load_stl(neighbors.second);
587 void Window::keyPressEvent(QKeyEvent* event)
589 if (!open_action->isEnabled())
591 QMainWindow::keyPressEvent(event);
595 if (event->key() == Qt::Key_Left)
600 else if (event->key() == Qt::Key_Right)
605 else if (event->key() == Qt::Key_Escape)
607 hide_menuBar_action->setChecked(false);
611 QMainWindow::keyPressEvent(event);