]> git.sur5r.net Git - fstl/blob - src/window.cpp
e9f3f94ebf2d980e1e088d876d8acf53ae89c699
[fstl] / src / window.cpp
1 #include <QMenuBar>
2
3 #include "window.h"
4 #include "canvas.h"
5 #include "loader.h"
6
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";
14
15 Window::Window(QWidget *parent) :
16     QMainWindow(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))
35
36 {
37     setWindowTitle("fstl");
38     setAcceptDrops(true);
39
40     QSurfaceFormat format;
41     format.setDepthBufferSize(24);
42     format.setStencilBufferSize(8);
43     format.setVersion(2, 1);
44     format.setProfile(QSurfaceFormat::CoreProfile);
45
46     QSurfaceFormat::setDefaultFormat(format);
47     
48     canvas = new Canvas(format, this);
49     setCentralWidget(canvas);
50
51     QObject::connect(watcher, &QFileSystemWatcher::fileChanged,
52                      this, &Window::on_watched_change);
53
54     open_action->setShortcut(QKeySequence::Open);
55     QObject::connect(open_action, &QAction::triggered,
56                      this, &Window::on_open);
57     this->addAction(open_action);
58
59     quit_action->setShortcut(QKeySequence::Quit);
60     QObject::connect(quit_action, &QAction::triggered,
61                      this, &Window::close);
62     this->addAction(quit_action);
63
64     autoreload_action->setCheckable(true);
65     QObject::connect(autoreload_action, &QAction::triggered,
66             this, &Window::on_autoreload_triggered);
67
68     reload_action->setShortcut(QKeySequence::Refresh);
69     reload_action->setEnabled(false);
70     QObject::connect(reload_action, &QAction::triggered,
71                      this, &Window::on_reload);
72
73     QObject::connect(about_action, &QAction::triggered,
74                      this, &Window::on_about);
75
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);
80
81     save_screenshot_action->setCheckable(false);
82     QObject::connect(save_screenshot_action, &QAction::triggered, 
83         this, &Window::on_save_screenshot);
84     
85     rebuild_recent_files();
86
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);
95
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})
102     {
103         projections->addAction(p);
104         p->setCheckable(true);
105     }
106     projections->setExclusive(true);
107     QObject::connect(projections, &QActionGroup::triggered,
108                      this, &Window::on_projection);
109
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})
116     {
117         drawModes->addAction(p);
118         p->setCheckable(true);
119     }
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);
127
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);       
132
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);
139
140     auto help_menu = menuBar()->addMenu("Help");
141     help_menu->addAction(about_action);
142
143     load_persist_settings();
144 }
145
146 void Window::load_persist_settings(){
147     QSettings 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);
151
152     autoreload_action->setChecked(settings.value(AUTORELOAD_KEY, true).toBool());
153
154     bool draw_axes = settings.value(DRAW_AXES_KEY, false).toBool();
155     canvas->draw_axes(draw_axes);
156     axes_action->setChecked(draw_axes);
157
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);
162     }else{
163         canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, false);
164         orthographic_action->setChecked(true);
165     }
166
167     DrawMode draw_mode = (DrawMode)settings.value(DRAW_MODE_KEY, DRAWMODECOUNT).toInt();
168     
169     if(draw_mode >= DRAWMODECOUNT)
170     {
171         draw_mode = shaded;
172     }
173     canvas->set_drawMode(draw_mode);
174     QAction* (dm_acts[]) = {shaded_action, wireframe_action, surfaceangle_action};
175     dm_acts[draw_mode]->setChecked(true);
176
177     resize(600, 400);
178     restoreGeometry(settings.value(WINDOW_GEOM_KEY).toByteArray());
179 }
180
181 void Window::on_open()
182 {
183     QString filename = QFileDialog::getOpenFileName(
184                 this, "Load .stl file", QString(), "STL files (*.stl, *.STL)");
185     if (!filename.isNull())
186     {
187         load_stl(filename);
188     }
189 }
190
191 void Window::on_about()
192 {
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>");
201 }
202
203 void Window::on_bad_stl()
204 {
205     QMessageBox::critical(this, "Error",
206                           "<b>Error:</b><br>"
207                           "This <code>.stl</code> file is invalid or corrupted.<br>"
208                           "Please export it from the original source, verify, and retry.");
209 }
210
211 void Window::on_empty_mesh()
212 {
213     QMessageBox::critical(this, "Error",
214                           "<b>Error:</b><br>"
215                           "This file is syntactically correct<br>but contains no triangles.");
216 }
217
218 void Window::on_missing_file()
219 {
220     QMessageBox::critical(this, "Error",
221                           "<b>Error:</b><br>"
222                           "The target file is missing.<br>");
223 }
224
225 void Window::enable_open()
226 {
227     open_action->setEnabled(true);
228 }
229
230 void Window::disable_open()
231 {
232     open_action->setEnabled(false);
233 }
234
235 void Window::set_watched(const QString& filename)
236 {
237     const auto files = watcher->files();
238     if (files.size())
239     {
240         watcher->removePaths(watcher->files());
241     }
242     watcher->addPath(filename);
243
244     QSettings settings;
245     auto recent = settings.value(RECENT_FILE_KEY).toStringList();
246     const auto f = QFileInfo(filename).absoluteFilePath();
247     recent.removeAll(f);
248     recent.prepend(f);
249     while (recent.size() > MAX_RECENT_FILES)
250     {
251         recent.pop_back();
252     }
253     settings.setValue(RECENT_FILE_KEY, recent);
254     rebuild_recent_files();
255 }
256
257 void Window::on_projection(QAction* proj)
258 {
259     if (proj == perspective_action)
260     {
261         canvas->view_perspective(Canvas::P_PERSPECTIVE, true);
262         QSettings().setValue(PROJECTION_KEY, "perspective");
263     }
264     else
265     {
266         canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, true);
267         QSettings().setValue(PROJECTION_KEY, "orthographic");
268     }
269 }
270
271 void Window::on_drawMode(QAction* act)
272 {
273     DrawMode mode;
274     if (act == shaded_action)
275     {
276         mode = shaded;
277     }
278     else if (act == wireframe_action)
279     {
280         mode = wireframe;
281     }
282     else
283     {
284         mode = surfaceangle;
285     }
286     canvas->set_drawMode(mode);
287     QSettings().setValue(DRAW_MODE_KEY, mode);
288 }
289
290 void Window::on_drawAxes(bool d)
291 {
292     canvas->draw_axes(d);
293     QSettings().setValue(DRAW_AXES_KEY, d);
294 }
295
296 void Window::on_invertZoom(bool d)
297 {
298     canvas->invert_zoom(d);
299     QSettings().setValue(INVERT_ZOOM_KEY, d);
300 }
301
302 void Window::on_watched_change(const QString& filename)
303 {
304     if (autoreload_action->isChecked())
305     {
306         load_stl(filename, true);
307     }
308 }
309
310 void Window::on_autoreload_triggered(bool b)
311 {
312     if (b)
313     {
314         on_reload();
315     }
316     QSettings().setValue(AUTORELOAD_KEY, b);
317 }
318
319 void Window::on_clear_recent()
320 {
321     QSettings settings;
322     settings.setValue(RECENT_FILE_KEY, QStringList());
323     rebuild_recent_files();
324 }
325
326 void Window::on_load_recent(QAction* a)
327 {
328     load_stl(a->data().toString());
329 }
330
331 void Window::on_loaded(const QString& filename)
332 {
333     current_file = filename;
334 }
335
336 void Window::on_save_screenshot()
337 {
338     const auto image = canvas->grabFramebuffer();
339     auto file_name = QFileDialog::getSaveFileName(
340         this, 
341         tr("Save Screenshot Image"),
342         QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(),
343         "Images (*.png *.jpg)");
344
345     auto get_file_extension = [](const std::string& file_name) -> std::string
346     {
347         const auto location = std::find(file_name.rbegin(), file_name.rend(), '.');
348         if (location == file_name.rend())
349         {
350             return "";
351         }
352
353         const auto index = std::distance(file_name.rbegin(), location);
354         return file_name.substr(file_name.size() - index);
355     };
356
357     const auto extension = get_file_extension(file_name.toStdString());
358     if(extension.empty() || (extension != "png" && extension != "jpg"))
359     {
360         file_name.append(".png");
361     }
362     
363     const auto save_ok = image.save(file_name);
364     if(!save_ok)
365     {
366         QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image."));
367     }
368 }
369
370 void Window::on_hide_menuBar()
371 {
372     menuBar()->setVisible(!hide_menuBar_action->isChecked());
373 }
374
375 void Window::rebuild_recent_files()
376 {
377     QSettings settings;
378     QStringList files = settings.value(RECENT_FILE_KEY).toStringList();
379
380     const auto actions = recent_files_group->actions();
381     for (auto a : actions)
382     {
383         recent_files_group->removeAction(a);
384     }
385     recent_files->clear();
386
387     for (auto f : files)
388     {
389         const auto a = new QAction(f, recent_files);
390         a->setData(f);
391         recent_files_group->addAction(a);
392         recent_files->addAction(a);
393     }
394     if (files.size() == 0)
395     {
396         auto a = new QAction("No recent files", recent_files);
397         recent_files->addAction(a);
398         a->setEnabled(false);
399     }
400     recent_files->addSeparator();
401     recent_files->addAction(recent_files_clear_action);
402 }
403
404 void Window::on_reload()
405 {
406     auto fs = watcher->files();
407     if (fs.size() == 1)
408     {
409         load_stl(fs[0], true);
410     }
411 }
412
413 bool Window::load_stl(const QString& filename, bool is_reload)
414 {
415     if (!open_action->isEnabled())  return false;
416
417     canvas->set_status("Loading " + filename);
418
419     Loader* loader = new Loader(this, filename, is_reload);
420     connect(loader, &Loader::started,
421               this, &Window::disable_open);
422
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);
431
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);
438
439     if (filename[0] != ':')
440     {
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);
448     }
449
450     loader->start();
451     return true;
452 }
453
454 void Window::dragEnterEvent(QDragEnterEvent *event)
455 {
456     if (event->mimeData()->hasUrls())
457     {
458         auto urls = event->mimeData()->urls();
459         if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
460             event->acceptProposedAction();
461     }
462 }
463
464 void Window::dropEvent(QDropEvent *event)
465 {
466     load_stl(event->mimeData()->urls().front().toLocalFile());
467 }
468
469 void Window::resizeEvent(QResizeEvent *event)
470 {
471     QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry());
472     QWidget::resizeEvent(event);
473 }
474
475 void Window::moveEvent(QMoveEvent *event)
476 {
477     QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry());
478     QWidget::moveEvent(event);
479 }
480
481 void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value)
482 {
483     int start = 0;
484     int end = list.size() - 1;
485     int index = 0;
486     while (start <= end){
487         int mid = (start+end)/2;
488         if (list[mid] == value) {
489             return;
490         }
491         int compare = collator.compare(value, list[mid]);
492         if (compare < 0) {
493             end = mid-1;
494             index = mid;
495         } else {
496             start = mid+1;
497             index = start;
498         }
499     }
500
501     list.insert(index, value);
502 }
503
504 void Window::build_folder_file_list()
505 {
506     QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath();
507     if (!lookup_folder_files.isEmpty())
508     {
509         if (current_folder_path == lookup_folder) {
510             return;
511         }
512
513         lookup_folder_files.clear();
514     }
515     lookup_folder = current_folder_path;
516
517     QCollator collator;
518     collator.setNumericMode(true);
519
520     QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden);
521     while (dirIterator.hasNext()) {
522         dirIterator.next();
523
524         QString name = dirIterator.fileName();
525         sorted_insert(lookup_folder_files, collator, name);
526     }
527 }
528
529 QPair<QString, QString> Window::get_file_neighbors()
530 {
531     if (current_file.isEmpty()) {
532         return QPair<QString, QString>(QString(), QString());
533     }
534
535     build_folder_file_list();
536
537     QFileInfo fileInfo(current_file);
538
539     QString current_dir = fileInfo.absoluteDir().absolutePath();
540     QString current_name = fileInfo.fileName();
541
542     QString prev = QString();
543     QString next = QString();
544
545     QListIterator<QString> fileIterator(lookup_folder_files);
546     while (fileIterator.hasNext()) {
547         QString name = fileIterator.next();
548
549         if (name == current_name) {
550             if (fileIterator.hasNext()) {
551                 next = current_dir + QDir::separator() + fileIterator.next();
552             }
553             break;
554         }
555
556         prev = name;
557     }
558
559     if (!prev.isEmpty()) {
560         prev.prepend(QDir::separator());
561         prev.prepend(current_dir);
562     }
563
564     return QPair<QString, QString>(prev, next);
565 }
566
567 bool Window::load_prev(void)
568 {
569     QPair<QString, QString> neighbors = get_file_neighbors();
570     if (neighbors.first.isEmpty()) {
571         return false;
572     }
573
574     return load_stl(neighbors.first);
575 }
576
577 bool Window::load_next(void)
578 {
579     QPair<QString, QString> neighbors = get_file_neighbors();
580     if (neighbors.second.isEmpty()) {
581         return false;
582     }
583
584     return load_stl(neighbors.second);
585 }
586
587 void Window::keyPressEvent(QKeyEvent* event)
588 {
589     if (!open_action->isEnabled())
590     {
591         QMainWindow::keyPressEvent(event);
592         return;
593     }
594
595     if (event->key() == Qt::Key_Left)
596     {
597         load_prev();
598         return;
599     }
600     else if (event->key() == Qt::Key_Right)
601     {
602         load_next();
603         return;
604     }
605     else if (event->key() == Qt::Key_Escape)
606     {
607         hide_menuBar_action->setChecked(false);
608         return;
609     }
610
611     QMainWindow::keyPressEvent(event);
612 }