7 const QString Window::RECENT_FILE_KEY = "recentFiles";
9 Window::Window(QWidget *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))
27 setWindowTitle("fstl");
30 QSurfaceFormat format;
31 format.setDepthBufferSize(24);
32 format.setStencilBufferSize(8);
33 format.setVersion(2, 1);
34 format.setProfile(QSurfaceFormat::CoreProfile);
36 QSurfaceFormat::setDefaultFormat(format);
38 canvas = new Canvas(format, this);
39 setCentralWidget(canvas);
41 QObject::connect(watcher, &QFileSystemWatcher::fileChanged,
42 this, &Window::on_watched_change);
44 open_action->setShortcut(QKeySequence::Open);
45 QObject::connect(open_action, &QAction::triggered,
46 this, &Window::on_open);
48 quit_action->setShortcut(QKeySequence::Quit);
49 QObject::connect(quit_action, &QAction::triggered,
50 this, &Window::close);
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);
58 reload_action->setShortcut(QKeySequence::Refresh);
59 reload_action->setEnabled(false);
60 QObject::connect(reload_action, &QAction::triggered,
61 this, &Window::on_reload);
63 QObject::connect(about_action, &QAction::triggered,
64 this, &Window::on_about);
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);
71 save_screenshot_action->setCheckable(false);
72 QObject::connect(save_screenshot_action, &QAction::triggered,
73 this, &Window::on_save_screenshot);
75 rebuild_recent_files();
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);
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})
93 projections->addAction(p);
94 p->setCheckable(true);
96 perspective_action->setChecked(true);
97 projections->setExclusive(true);
98 QObject::connect(projections, &QActionGroup::triggered,
99 this, &Window::on_projection);
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})
107 drawModes->addAction(p);
108 p->setCheckable(true);
110 shaded_action->setChecked(true);
111 drawModes->setExclusive(true);
112 QObject::connect(drawModes, &QActionGroup::triggered,
113 this, &Window::on_drawMode);
115 auto help_menu = menuBar()->addMenu("Help");
116 help_menu->addAction(about_action);
121 void Window::on_open()
123 QString filename = QFileDialog::getOpenFileName(
124 this, "Load .stl file", QString(), "*.stl");
125 if (!filename.isNull())
131 void Window::on_about()
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>");
143 void Window::on_bad_stl()
145 QMessageBox::critical(this, "Error",
147 "This <code>.stl</code> file is invalid or corrupted.<br>"
148 "Please export it from the original source, verify, and retry.");
151 void Window::on_empty_mesh()
153 QMessageBox::critical(this, "Error",
155 "This file is syntactically correct<br>but contains no triangles.");
158 void Window::on_confusing_stl()
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.");
166 void Window::on_missing_file()
168 QMessageBox::critical(this, "Error",
170 "The target file is missing.<br>");
173 void Window::enable_open()
175 open_action->setEnabled(true);
178 void Window::disable_open()
180 open_action->setEnabled(false);
183 void Window::set_watched(const QString& filename)
185 const auto files = watcher->files();
188 watcher->removePaths(watcher->files());
190 watcher->addPath(filename);
193 auto recent = settings.value(RECENT_FILE_KEY).toStringList();
194 const auto f = QFileInfo(filename).absoluteFilePath();
197 while (recent.size() > MAX_RECENT_FILES)
201 settings.setValue(RECENT_FILE_KEY, recent);
202 rebuild_recent_files();
205 void Window::on_projection(QAction* proj)
207 if (proj == perspective_action)
209 canvas->view_perspective();
213 canvas->view_orthographic();
217 void Window::on_drawMode(QAction* mode)
219 if (mode == shaded_action)
221 canvas->draw_shaded();
225 canvas->draw_wireframe();
229 void Window::on_watched_change(const QString& filename)
231 if (autoreload_action->isChecked())
233 load_stl(filename, true);
237 void Window::on_autoreload_triggered(bool b)
245 void Window::on_clear_recent()
248 settings.setValue(RECENT_FILE_KEY, QStringList());
249 rebuild_recent_files();
252 void Window::on_load_recent(QAction* a)
254 load_stl(a->data().toString());
257 void Window::on_loaded(const QString& filename)
259 current_file = filename;
262 void Window::on_save_screenshot()
264 const auto image = canvas->grabFramebuffer();
265 auto file_name = QFileDialog::getSaveFileName(
267 tr("Save Screenshot Image"),
268 QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(),
269 "Images (*.png *.jpg)");
271 auto get_file_extension = [](const std::string& file_name) -> std::string
273 const auto location = std::find(file_name.rbegin(), file_name.rend(), '.');
274 if (location == file_name.rend())
279 const auto index = std::distance(file_name.rbegin(), location);
280 return file_name.substr(file_name.size() - index);
283 const auto extension = get_file_extension(file_name.toStdString());
284 if(extension.empty() || (extension != "png" && extension != "jpg"))
286 file_name.append(".png");
289 const auto save_ok = image.save(file_name);
292 QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image."));
296 void Window::rebuild_recent_files()
299 QStringList files = settings.value(RECENT_FILE_KEY).toStringList();
301 const auto actions = recent_files_group->actions();
302 for (auto a : actions)
304 recent_files_group->removeAction(a);
306 recent_files->clear();
310 const auto a = new QAction(f, recent_files);
312 recent_files_group->addAction(a);
313 recent_files->addAction(a);
315 if (files.size() == 0)
317 auto a = new QAction("No recent files", recent_files);
318 recent_files->addAction(a);
319 a->setEnabled(false);
321 recent_files->addSeparator();
322 recent_files->addAction(recent_files_clear_action);
325 void Window::on_reload()
327 auto fs = watcher->files();
330 load_stl(fs[0], true);
334 bool Window::load_stl(const QString& filename, bool is_reload)
336 if (!open_action->isEnabled()) return false;
338 canvas->set_status("Loading " + filename);
340 Loader* loader = new Loader(this, filename, is_reload);
341 connect(loader, &Loader::started,
342 this, &Window::disable_open);
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);
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);
362 if (filename[0] != ':')
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);
378 void Window::dragEnterEvent(QDragEnterEvent *event)
380 if (event->mimeData()->hasUrls())
382 auto urls = event->mimeData()->urls();
383 if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
384 event->acceptProposedAction();
388 void Window::dropEvent(QDropEvent *event)
390 load_stl(event->mimeData()->urls().front().toLocalFile());
393 void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value)
396 int end = list.size() - 1;
398 while (start <= end){
399 int mid = (start+end)/2;
400 if (list[mid] == value) {
403 int compare = collator.compare(value, list[mid]);
413 list.insert(index, value);
416 void Window::build_folder_file_list()
418 QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath();
419 if (!lookup_folder_files.isEmpty())
421 if (current_folder_path == lookup_folder) {
425 lookup_folder_files.clear();
427 lookup_folder = current_folder_path;
430 collator.setNumericMode(true);
432 QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden);
433 while (dirIterator.hasNext()) {
436 QString name = dirIterator.fileName();
437 sorted_insert(lookup_folder_files, collator, name);
441 QPair<QString, QString> Window::get_file_neighbors()
443 if (current_file.isEmpty()) {
444 return QPair<QString, QString>(QString::null, QString::null);
447 build_folder_file_list();
449 QFileInfo fileInfo(current_file);
451 QString current_dir = fileInfo.absoluteDir().absolutePath();
452 QString current_name = fileInfo.fileName();
454 QString prev = QString::null;
455 QString next = QString::null;
457 QListIterator<QString> fileIterator(lookup_folder_files);
458 while (fileIterator.hasNext()) {
459 QString name = fileIterator.next();
461 if (name == current_name) {
462 if (fileIterator.hasNext()) {
463 next = current_dir + QDir::separator() + fileIterator.next();
471 if (!prev.isEmpty()) {
472 prev.prepend(QDir::separator());
473 prev.prepend(current_dir);
476 return QPair<QString, QString>(prev, next);
479 bool Window::load_prev(void)
481 QPair<QString, QString> neighbors = get_file_neighbors();
482 if (neighbors.first.isEmpty()) {
486 return load_stl(neighbors.first);
489 bool Window::load_next(void)
491 QPair<QString, QString> neighbors = get_file_neighbors();
492 if (neighbors.second.isEmpty()) {
496 return load_stl(neighbors.second);
499 void Window::keyPressEvent(QKeyEvent* event)
501 if (!open_action->isEnabled())
503 QMainWindow::keyPressEvent(event);
507 if (event->key() == Qt::Key_Left)
512 else if (event->key() == Qt::Key_Right)
518 QMainWindow::keyPressEvent(event);