9 const QString Window::RECENT_FILE_KEY = "recentFiles";
11 Window::Window(QWidget *parent) :
13 open_action(new QAction("Open", this)),
14 about_action(new QAction("About", this)),
15 quit_action(new QAction("Quit", this)),
16 perspective_action(new QAction("Perspective", this)),
17 orthogonal_action(new QAction("Orthographic", this)),
18 reload_action(new QAction("Reload", this)),
19 autoreload_action(new QAction("Autoreload", this)),
20 recent_files(new QMenu("Open recent", this)),
21 recent_files_group(new QActionGroup(this)),
22 recent_files_clear_action(new QAction("Clear recent files", this)),
23 watcher(new QFileSystemWatcher(this))
26 setWindowTitle("fstl");
30 format.setVersion(2, 1);
31 format.setSampleBuffers(true);
33 canvas = new Canvas(format, this);
34 setCentralWidget(canvas);
36 QObject::connect(watcher, &QFileSystemWatcher::fileChanged,
37 this, &Window::on_watched_change);
39 open_action->setShortcut(QKeySequence::Open);
40 QObject::connect(open_action, &QAction::triggered,
41 this, &Window::on_open);
43 quit_action->setShortcut(QKeySequence::Quit);
44 QObject::connect(quit_action, &QAction::triggered,
45 this, &Window::close);
47 autoreload_action->setCheckable(true);
48 autoreload_action->setChecked(true);
49 autoreload_action->setEnabled(false);
50 QObject::connect(autoreload_action, &QAction::triggered,
51 this, &Window::on_autoreload_triggered);
53 reload_action->setShortcut(QKeySequence::Refresh);
54 reload_action->setEnabled(false);
55 QObject::connect(reload_action, &QAction::triggered,
56 this, &Window::on_reload);
58 QObject::connect(about_action, &QAction::triggered,
59 this, &Window::on_about);
61 QObject::connect(recent_files_clear_action, &QAction::triggered,
62 this, &Window::on_clear_recent);
63 QObject::connect(recent_files_group, &QActionGroup::triggered,
64 this, &Window::on_load_recent);
66 rebuild_recent_files();
68 auto file_menu = menuBar()->addMenu("File");
69 file_menu->addAction(open_action);
70 file_menu->addMenu(recent_files);
71 file_menu->addSeparator();
72 file_menu->addAction(reload_action);
73 file_menu->addAction(autoreload_action);
74 file_menu->addAction(quit_action);
76 auto view_menu = menuBar()->addMenu("View");
77 auto projection_menu = view_menu->addMenu("Projection");
78 projection_menu->addAction(perspective_action);
79 projection_menu->addAction(orthogonal_action);
80 auto projections = new QActionGroup(projection_menu);
81 for (auto p : {perspective_action, orthogonal_action})
83 projections->addAction(p);
84 p->setCheckable(true);
86 perspective_action->setChecked(true);
87 projections->setExclusive(true);
88 QObject::connect(projections, &QActionGroup::triggered,
89 this, &Window::on_projection);
91 auto help_menu = menuBar()->addMenu("Help");
92 help_menu->addAction(about_action);
97 void Window::on_open()
99 QString filename = QFileDialog::getOpenFileName(
100 this, "Load .stl file", QString(), "*.stl");
101 if (not filename.isNull())
107 void Window::on_about()
109 QMessageBox::about(this, "",
110 "<p align=\"center\"><b>fstl</b></p>"
111 "<p>A fast viewer for <code>.stl</code> files.<br>"
112 "<a href=\"https://github.com/mkeeter/fstl\""
113 " style=\"color: #93a1a1;\">https://github.com/mkeeter/fstl</a></p>"
114 "<p>© 2014-2017 Matthew Keeter<br>"
115 "<a href=\"mailto:matt.j.keeter@gmail.com\""
116 " style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
119 void Window::on_bad_stl()
121 QMessageBox::critical(this, "Error",
123 "This <code>.stl</code> file is invalid or corrupted.<br>"
124 "Please export it from the original source, verify, and retry.");
127 void Window::on_empty_mesh()
129 QMessageBox::critical(this, "Error",
131 "This file is syntactically correct<br>but contains no triangles.");
134 void Window::on_confusing_stl()
136 QMessageBox::warning(this, "Warning",
137 "<b>Warning:</b><br>"
138 "This <code>.stl</code> file begins with <code>solid </code>but appears to be a binary file.<br>"
139 "<code>fstl</code> loaded it, but other programs may be confused by this file.");
142 void Window::on_missing_file()
144 QMessageBox::critical(this, "Error",
146 "The target file is missing.<br>");
149 void Window::enable_open()
151 open_action->setEnabled(true);
154 void Window::disable_open()
156 open_action->setEnabled(false);
159 void Window::set_watched(const QString& filename)
161 const auto files = watcher->files();
164 watcher->removePaths(watcher->files());
166 watcher->addPath(filename);
169 auto recent = settings.value(RECENT_FILE_KEY).toStringList();
170 const auto f = QFileInfo(filename).absoluteFilePath();
173 while (recent.size() > MAX_RECENT_FILES)
177 settings.setValue(RECENT_FILE_KEY, recent);
178 rebuild_recent_files();
181 void Window::on_projection(QAction* proj)
183 if (proj == perspective_action)
185 canvas->view_perspective();
189 canvas->view_orthographic();
193 void Window::on_watched_change(const QString& filename)
195 if (autoreload_action->isChecked())
197 load_stl(filename, true);
201 void Window::on_autoreload_triggered(bool b)
209 void Window::on_clear_recent()
212 settings.setValue(RECENT_FILE_KEY, QStringList());
213 rebuild_recent_files();
216 void Window::on_load_recent(QAction* a)
218 load_stl(a->data().toString());
221 void Window::rebuild_recent_files()
224 QStringList files = settings.value(RECENT_FILE_KEY).toStringList();
226 const auto actions = recent_files_group->actions();
227 for (auto a : actions)
229 recent_files_group->removeAction(a);
231 recent_files->clear();
235 const auto a = new QAction(f, recent_files);
237 recent_files_group->addAction(a);
238 recent_files->addAction(a);
240 if (files.size() == 0)
242 auto a = new QAction("No recent files", recent_files);
243 recent_files->addAction(a);
244 a->setEnabled(false);
246 recent_files->addSeparator();
247 recent_files->addAction(recent_files_clear_action);
250 void Window::on_reload()
252 auto fs = watcher->files();
255 load_stl(fs[0], true);
259 bool Window::load_stl(const QString& filename, bool is_reload)
261 if (!open_action->isEnabled()) return false;
263 canvas->set_status("Loading " + filename);
265 Loader* loader = new Loader(this, filename, is_reload);
266 connect(loader, &Loader::started,
267 this, &Window::disable_open);
269 connect(loader, &Loader::got_mesh,
270 canvas, &Canvas::load_mesh);
271 connect(loader, &Loader::error_bad_stl,
272 this, &Window::on_bad_stl);
273 connect(loader, &Loader::error_empty_mesh,
274 this, &Window::on_empty_mesh);
275 connect(loader, &Loader::warning_confusing_stl,
276 this, &Window::on_confusing_stl);
277 connect(loader, &Loader::error_missing_file,
278 this, &Window::on_missing_file);
280 connect(loader, &Loader::finished,
281 loader, &Loader::deleteLater);
282 connect(loader, &Loader::finished,
283 this, &Window::enable_open);
284 connect(loader, &Loader::finished,
285 canvas, &Canvas::clear_status);
287 if (filename[0] != ':')
289 connect(loader, &Loader::loaded_file,
290 this, &Window::setWindowTitle);
291 connect(loader, &Loader::loaded_file,
292 this, &Window::set_watched);
293 autoreload_action->setEnabled(true);
294 reload_action->setEnabled(true);
301 void Window::dragEnterEvent(QDragEnterEvent *event)
303 if (event->mimeData()->hasUrls())
305 auto urls = event->mimeData()->urls();
306 if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
307 event->acceptProposedAction();
311 void Window::dropEvent(QDropEvent *event)
313 load_stl(event->mimeData()->urls().front().toLocalFile());