]> git.sur5r.net Git - fstl/blob - src/window.cpp
New upstream version 0.9.4
[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
9 Window::Window(QWidget *parent) :
10     QMainWindow(parent),
11     open_action(new QAction("Open", this)),
12     about_action(new QAction("About", this)),
13     quit_action(new QAction("Quit", this)),
14     perspective_action(new QAction("Perspective", this)),
15     orthogonal_action(new QAction("Orthographic", this)),
16     shaded_action(new QAction("Shaded", this)),
17     wireframe_action(new QAction("Wireframe", this)),
18     reload_action(new QAction("Reload", this)),
19     autoreload_action(new QAction("Autoreload", this)),
20     save_screenshot_action(new QAction("Save Screenshot", this)),
21     recent_files(new QMenu("Open recent", this)),
22     recent_files_group(new QActionGroup(this)),
23     recent_files_clear_action(new QAction("Clear recent files", this)),
24     watcher(new QFileSystemWatcher(this))
25
26 {
27     setWindowTitle("fstl");
28     setAcceptDrops(true);
29
30     QSurfaceFormat format;
31     format.setDepthBufferSize(24);
32     format.setStencilBufferSize(8);
33     format.setVersion(2, 1);
34     format.setProfile(QSurfaceFormat::CoreProfile);
35
36     QSurfaceFormat::setDefaultFormat(format);
37     
38     canvas = new Canvas(format, this);
39     setCentralWidget(canvas);
40
41     QObject::connect(watcher, &QFileSystemWatcher::fileChanged,
42                      this, &Window::on_watched_change);
43
44     open_action->setShortcut(QKeySequence::Open);
45     QObject::connect(open_action, &QAction::triggered,
46                      this, &Window::on_open);
47
48     quit_action->setShortcut(QKeySequence::Quit);
49     QObject::connect(quit_action, &QAction::triggered,
50                      this, &Window::close);
51
52     autoreload_action->setCheckable(true);
53     autoreload_action->setChecked(true);
54     autoreload_action->setEnabled(false);
55     QObject::connect(autoreload_action, &QAction::triggered,
56             this, &Window::on_autoreload_triggered);
57
58     reload_action->setShortcut(QKeySequence::Refresh);
59     reload_action->setEnabled(false);
60     QObject::connect(reload_action, &QAction::triggered,
61                      this, &Window::on_reload);
62
63     QObject::connect(about_action, &QAction::triggered,
64                      this, &Window::on_about);
65
66     QObject::connect(recent_files_clear_action, &QAction::triggered,
67                      this, &Window::on_clear_recent);
68     QObject::connect(recent_files_group, &QActionGroup::triggered,
69                      this, &Window::on_load_recent);
70
71     save_screenshot_action->setCheckable(false);
72     QObject::connect(save_screenshot_action, &QAction::triggered, 
73         this, &Window::on_save_screenshot);
74     
75     rebuild_recent_files();
76
77     auto file_menu = menuBar()->addMenu("File");
78     file_menu->addAction(open_action);
79     file_menu->addMenu(recent_files);
80     file_menu->addSeparator();
81     file_menu->addAction(reload_action);
82     file_menu->addAction(autoreload_action);
83     file_menu->addAction(save_screenshot_action);
84     file_menu->addAction(quit_action);
85
86     auto view_menu = menuBar()->addMenu("View");
87     auto projection_menu = view_menu->addMenu("Projection");
88     projection_menu->addAction(perspective_action);
89     projection_menu->addAction(orthogonal_action);
90     auto projections = new QActionGroup(projection_menu);
91     for (auto p : {perspective_action, orthogonal_action})
92     {
93         projections->addAction(p);
94         p->setCheckable(true);
95     }
96     perspective_action->setChecked(true);
97     projections->setExclusive(true);
98     QObject::connect(projections, &QActionGroup::triggered,
99                      this, &Window::on_projection);
100
101     auto draw_menu = view_menu->addMenu("Draw Mode");
102     draw_menu->addAction(shaded_action);
103     draw_menu->addAction(wireframe_action);
104     auto drawModes = new QActionGroup(draw_menu);
105     for (auto p : {shaded_action, wireframe_action})
106     {
107         drawModes->addAction(p);
108         p->setCheckable(true);
109     }
110     shaded_action->setChecked(true);
111     drawModes->setExclusive(true);
112     QObject::connect(drawModes, &QActionGroup::triggered,
113                      this, &Window::on_drawMode);
114
115     auto help_menu = menuBar()->addMenu("Help");
116     help_menu->addAction(about_action);
117
118     resize(600, 400);
119 }
120
121 void Window::on_open()
122 {
123     QString filename = QFileDialog::getOpenFileName(
124                 this, "Load .stl file", QString(), "*.stl");
125     if (!filename.isNull())
126     {
127         load_stl(filename);
128     }
129 }
130
131 void Window::on_about()
132 {
133     QMessageBox::about(this, "",
134         "<p align=\"center\"><b>fstl</b></p>"
135         "<p>A fast viewer for <code>.stl</code> files.<br>"
136         "<a href=\"https://github.com/mkeeter/fstl\""
137         "   style=\"color: #93a1a1;\">https://github.com/mkeeter/fstl</a></p>"
138         "<p>© 2014-2017 Matthew Keeter<br>"
139         "<a href=\"mailto:matt.j.keeter@gmail.com\""
140         "   style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
141 }
142
143 void Window::on_bad_stl()
144 {
145     QMessageBox::critical(this, "Error",
146                           "<b>Error:</b><br>"
147                           "This <code>.stl</code> file is invalid or corrupted.<br>"
148                           "Please export it from the original source, verify, and retry.");
149 }
150
151 void Window::on_empty_mesh()
152 {
153     QMessageBox::critical(this, "Error",
154                           "<b>Error:</b><br>"
155                           "This file is syntactically correct<br>but contains no triangles.");
156 }
157
158 void Window::on_confusing_stl()
159 {
160     QMessageBox::warning(this, "Warning",
161                          "<b>Warning:</b><br>"
162                          "This <code>.stl</code> file begins with <code>solid </code>but appears to be a binary file.<br>"
163                          "<code>fstl</code> loaded it, but other programs may be confused by this file.");
164 }
165
166 void Window::on_missing_file()
167 {
168     QMessageBox::critical(this, "Error",
169                           "<b>Error:</b><br>"
170                           "The target file is missing.<br>");
171 }
172
173 void Window::enable_open()
174 {
175     open_action->setEnabled(true);
176 }
177
178 void Window::disable_open()
179 {
180     open_action->setEnabled(false);
181 }
182
183 void Window::set_watched(const QString& filename)
184 {
185     const auto files = watcher->files();
186     if (files.size())
187     {
188         watcher->removePaths(watcher->files());
189     }
190     watcher->addPath(filename);
191
192     QSettings settings;
193     auto recent = settings.value(RECENT_FILE_KEY).toStringList();
194     const auto f = QFileInfo(filename).absoluteFilePath();
195     recent.removeAll(f);
196     recent.prepend(f);
197     while (recent.size() > MAX_RECENT_FILES)
198     {
199         recent.pop_back();
200     }
201     settings.setValue(RECENT_FILE_KEY, recent);
202     rebuild_recent_files();
203 }
204
205 void Window::on_projection(QAction* proj)
206 {
207     if (proj == perspective_action)
208     {
209         canvas->view_perspective();
210     }
211     else
212     {
213         canvas->view_orthographic();
214     }
215 }
216
217 void Window::on_drawMode(QAction* mode)
218 {
219     if (mode == shaded_action)
220     {
221         canvas->draw_shaded();
222     }
223     else
224     {
225         canvas->draw_wireframe();
226     }
227 }
228
229 void Window::on_watched_change(const QString& filename)
230 {
231     if (autoreload_action->isChecked())
232     {
233         load_stl(filename, true);
234     }
235 }
236
237 void Window::on_autoreload_triggered(bool b)
238 {
239     if (b)
240     {
241         on_reload();
242     }
243 }
244
245 void Window::on_clear_recent()
246 {
247     QSettings settings;
248     settings.setValue(RECENT_FILE_KEY, QStringList());
249     rebuild_recent_files();
250 }
251
252 void Window::on_load_recent(QAction* a)
253 {
254     load_stl(a->data().toString());
255 }
256
257 void Window::on_loaded(const QString& filename)
258 {
259     current_file = filename;
260 }
261
262 void Window::on_save_screenshot()
263 {
264     const auto image = canvas->grabFramebuffer();
265     auto file_name = QFileDialog::getSaveFileName(
266         this, 
267         tr("Save Screenshot Image"),
268         QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(),
269         "Images (*.png *.jpg)");
270
271     auto get_file_extension = [](const std::string& file_name) -> std::string
272     {
273         const auto location = std::find(file_name.rbegin(), file_name.rend(), '.');
274         if (location == file_name.rend())
275         {
276             return "";
277         }
278
279         const auto index = std::distance(file_name.rbegin(), location);
280         return file_name.substr(file_name.size() - index);
281     };
282
283     const auto extension = get_file_extension(file_name.toStdString());
284     if(extension.empty() || (extension != "png" && extension != "jpg"))
285     {
286         file_name.append(".png");
287     }
288     
289     const auto save_ok = image.save(file_name);
290     if(!save_ok)
291     {
292         QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image."));
293     }
294 }
295
296 void Window::rebuild_recent_files()
297 {
298     QSettings settings;
299     QStringList files = settings.value(RECENT_FILE_KEY).toStringList();
300
301     const auto actions = recent_files_group->actions();
302     for (auto a : actions)
303     {
304         recent_files_group->removeAction(a);
305     }
306     recent_files->clear();
307
308     for (auto f : files)
309     {
310         const auto a = new QAction(f, recent_files);
311         a->setData(f);
312         recent_files_group->addAction(a);
313         recent_files->addAction(a);
314     }
315     if (files.size() == 0)
316     {
317         auto a = new QAction("No recent files", recent_files);
318         recent_files->addAction(a);
319         a->setEnabled(false);
320     }
321     recent_files->addSeparator();
322     recent_files->addAction(recent_files_clear_action);
323 }
324
325 void Window::on_reload()
326 {
327     auto fs = watcher->files();
328     if (fs.size() == 1)
329     {
330         load_stl(fs[0], true);
331     }
332 }
333
334 bool Window::load_stl(const QString& filename, bool is_reload)
335 {
336     if (!open_action->isEnabled())  return false;
337
338     canvas->set_status("Loading " + filename);
339
340     Loader* loader = new Loader(this, filename, is_reload);
341     connect(loader, &Loader::started,
342               this, &Window::disable_open);
343
344     connect(loader, &Loader::got_mesh,
345             canvas, &Canvas::load_mesh);
346     connect(loader, &Loader::error_bad_stl,
347               this, &Window::on_bad_stl);
348     connect(loader, &Loader::error_empty_mesh,
349               this, &Window::on_empty_mesh);
350     connect(loader, &Loader::warning_confusing_stl,
351               this, &Window::on_confusing_stl);
352     connect(loader, &Loader::error_missing_file,
353               this, &Window::on_missing_file);
354
355     connect(loader, &Loader::finished,
356             loader, &Loader::deleteLater);
357     connect(loader, &Loader::finished,
358               this, &Window::enable_open);
359     connect(loader, &Loader::finished,
360             canvas, &Canvas::clear_status);
361
362     if (filename[0] != ':')
363     {
364         connect(loader, &Loader::loaded_file,
365                   this, &Window::setWindowTitle);
366         connect(loader, &Loader::loaded_file,
367                   this, &Window::set_watched);
368         connect(loader, &Loader::loaded_file,
369                   this, &Window::on_loaded);
370         autoreload_action->setEnabled(true);
371         reload_action->setEnabled(true);
372     }
373
374     loader->start();
375     return true;
376 }
377
378 void Window::dragEnterEvent(QDragEnterEvent *event)
379 {
380     if (event->mimeData()->hasUrls())
381     {
382         auto urls = event->mimeData()->urls();
383         if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
384             event->acceptProposedAction();
385     }
386 }
387
388 void Window::dropEvent(QDropEvent *event)
389 {
390     load_stl(event->mimeData()->urls().front().toLocalFile());
391 }
392
393 void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value)
394 {
395     int start = 0;
396     int end = list.size() - 1;
397     int index = 0;
398     while (start <= end){
399         int mid = (start+end)/2;
400         if (list[mid] == value) {
401             return;
402         }
403         int compare = collator.compare(value, list[mid]);
404         if (compare < 0) {
405             end = mid-1;
406             index = mid;
407         } else {
408             start = mid+1;
409             index = start;
410         }
411     }
412
413     list.insert(index, value);
414 }
415
416 void Window::build_folder_file_list()
417 {
418     QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath();
419     if (!lookup_folder_files.isEmpty())
420     {
421         if (current_folder_path == lookup_folder) {
422             return;
423         }
424
425         lookup_folder_files.clear();
426     }
427     lookup_folder = current_folder_path;
428
429     QCollator collator;
430     collator.setNumericMode(true);
431
432     QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden);
433     while (dirIterator.hasNext()) {
434         dirIterator.next();
435
436         QString name = dirIterator.fileName();
437         sorted_insert(lookup_folder_files, collator, name);
438     }
439 }
440
441 QPair<QString, QString> Window::get_file_neighbors()
442 {
443     if (current_file.isEmpty()) {
444         return QPair<QString, QString>(QString::null, QString::null);
445     }
446
447     build_folder_file_list();
448
449     QFileInfo fileInfo(current_file);
450
451     QString current_dir = fileInfo.absoluteDir().absolutePath();
452     QString current_name = fileInfo.fileName();
453
454     QString prev = QString::null;
455     QString next = QString::null;
456
457     QListIterator<QString> fileIterator(lookup_folder_files);
458     while (fileIterator.hasNext()) {
459         QString name = fileIterator.next();
460
461         if (name == current_name) {
462             if (fileIterator.hasNext()) {
463                 next = current_dir + QDir::separator() + fileIterator.next();
464             }
465             break;
466         }
467
468         prev = name;
469     }
470
471     if (!prev.isEmpty()) {
472         prev.prepend(QDir::separator());
473         prev.prepend(current_dir);
474     }
475
476     return QPair<QString, QString>(prev, next);
477 }
478
479 bool Window::load_prev(void)
480 {
481     QPair<QString, QString> neighbors = get_file_neighbors();
482     if (neighbors.first.isEmpty()) {
483         return false;
484     }
485
486     return load_stl(neighbors.first);
487 }
488
489 bool Window::load_next(void)
490 {
491     QPair<QString, QString> neighbors = get_file_neighbors();
492     if (neighbors.second.isEmpty()) {
493         return false;
494     }
495
496     return load_stl(neighbors.second);
497 }
498
499 void Window::keyPressEvent(QKeyEvent* event)
500 {
501     if (!open_action->isEnabled())
502     {
503         QMainWindow::keyPressEvent(event);
504         return;
505     }
506
507     if (event->key() == Qt::Key_Left)
508     {
509         load_prev();
510         return;
511     }
512     else if (event->key() == Qt::Key_Right)
513     {
514         load_next();
515         return;
516     }
517
518     QMainWindow::keyPressEvent(event);
519 }