]> git.sur5r.net Git - fstl/blob - src/window.cpp
New upstream version 0.9.3
[fstl] / src / window.cpp
1 #include <QMenuBar>
2 #include <QMessageBox>
3 #include <QFileDialog>
4
5 #include "window.h"
6 #include "canvas.h"
7 #include "loader.h"
8
9 const QString Window::RECENT_FILE_KEY = "recentFiles";
10
11 Window::Window(QWidget *parent) :
12     QMainWindow(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))
24
25 {
26     setWindowTitle("fstl");
27     setAcceptDrops(true);
28
29     QGLFormat format;
30     format.setVersion(2, 1);
31     format.setSampleBuffers(true);
32
33     canvas = new Canvas(format, this);
34     setCentralWidget(canvas);
35
36     QObject::connect(watcher, &QFileSystemWatcher::fileChanged,
37                      this, &Window::on_watched_change);
38
39     open_action->setShortcut(QKeySequence::Open);
40     QObject::connect(open_action, &QAction::triggered,
41                      this, &Window::on_open);
42
43     quit_action->setShortcut(QKeySequence::Quit);
44     QObject::connect(quit_action, &QAction::triggered,
45                      this, &Window::close);
46
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);
52
53     reload_action->setShortcut(QKeySequence::Refresh);
54     reload_action->setEnabled(false);
55     QObject::connect(reload_action, &QAction::triggered,
56                      this, &Window::on_reload);
57
58     QObject::connect(about_action, &QAction::triggered,
59                      this, &Window::on_about);
60
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);
65
66     rebuild_recent_files();
67
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);
75
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})
82     {
83         projections->addAction(p);
84         p->setCheckable(true);
85     }
86     perspective_action->setChecked(true);
87     projections->setExclusive(true);
88     QObject::connect(projections, &QActionGroup::triggered,
89                      this, &Window::on_projection);
90
91     auto help_menu = menuBar()->addMenu("Help");
92     help_menu->addAction(about_action);
93
94     resize(600, 400);
95 }
96
97 void Window::on_open()
98 {
99     QString filename = QFileDialog::getOpenFileName(
100                 this, "Load .stl file", QString(), "*.stl");
101     if (not filename.isNull())
102     {
103         load_stl(filename);
104     }
105 }
106
107 void Window::on_about()
108 {
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>");
117 }
118
119 void Window::on_bad_stl()
120 {
121     QMessageBox::critical(this, "Error",
122                           "<b>Error:</b><br>"
123                           "This <code>.stl</code> file is invalid or corrupted.<br>"
124                           "Please export it from the original source, verify, and retry.");
125 }
126
127 void Window::on_empty_mesh()
128 {
129     QMessageBox::critical(this, "Error",
130                           "<b>Error:</b><br>"
131                           "This file is syntactically correct<br>but contains no triangles.");
132 }
133
134 void Window::on_confusing_stl()
135 {
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.");
140 }
141
142 void Window::on_missing_file()
143 {
144     QMessageBox::critical(this, "Error",
145                           "<b>Error:</b><br>"
146                           "The target file is missing.<br>");
147 }
148
149 void Window::enable_open()
150 {
151     open_action->setEnabled(true);
152 }
153
154 void Window::disable_open()
155 {
156     open_action->setEnabled(false);
157 }
158
159 void Window::set_watched(const QString& filename)
160 {
161     const auto files = watcher->files();
162     if (files.size())
163     {
164         watcher->removePaths(watcher->files());
165     }
166     watcher->addPath(filename);
167
168     QSettings settings;
169     auto recent = settings.value(RECENT_FILE_KEY).toStringList();
170     const auto f = QFileInfo(filename).absoluteFilePath();
171     recent.removeAll(f);
172     recent.prepend(f);
173     while (recent.size() > MAX_RECENT_FILES)
174     {
175         recent.pop_back();
176     }
177     settings.setValue(RECENT_FILE_KEY, recent);
178     rebuild_recent_files();
179 }
180
181 void Window::on_projection(QAction* proj)
182 {
183     if (proj == perspective_action)
184     {
185         canvas->view_perspective();
186     }
187     else
188     {
189         canvas->view_orthographic();
190     }
191 }
192
193 void Window::on_watched_change(const QString& filename)
194 {
195     if (autoreload_action->isChecked())
196     {
197         load_stl(filename, true);
198     }
199 }
200
201 void Window::on_autoreload_triggered(bool b)
202 {
203     if (b)
204     {
205         on_reload();
206     }
207 }
208
209 void Window::on_clear_recent()
210 {
211     QSettings settings;
212     settings.setValue(RECENT_FILE_KEY, QStringList());
213     rebuild_recent_files();
214 }
215
216 void Window::on_load_recent(QAction* a)
217 {
218     load_stl(a->data().toString());
219 }
220
221 void Window::rebuild_recent_files()
222 {
223     QSettings settings;
224     QStringList files = settings.value(RECENT_FILE_KEY).toStringList();
225
226     const auto actions = recent_files_group->actions();
227     for (auto a : actions)
228     {
229         recent_files_group->removeAction(a);
230     }
231     recent_files->clear();
232
233     for (auto f : files)
234     {
235         const auto a = new QAction(f, recent_files);
236         a->setData(f);
237         recent_files_group->addAction(a);
238         recent_files->addAction(a);
239     }
240     if (files.size() == 0)
241     {
242         auto a = new QAction("No recent files", recent_files);
243         recent_files->addAction(a);
244         a->setEnabled(false);
245     }
246     recent_files->addSeparator();
247     recent_files->addAction(recent_files_clear_action);
248 }
249
250 void Window::on_reload()
251 {
252     auto fs = watcher->files();
253     if (fs.size() == 1)
254     {
255         load_stl(fs[0], true);
256     }
257 }
258
259 bool Window::load_stl(const QString& filename, bool is_reload)
260 {
261     if (!open_action->isEnabled())  return false;
262
263     canvas->set_status("Loading " + filename);
264
265     Loader* loader = new Loader(this, filename, is_reload);
266     connect(loader, &Loader::started,
267               this, &Window::disable_open);
268
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);
279
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);
286
287     if (filename[0] != ':')
288     {
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);
295     }
296
297     loader->start();
298     return true;
299 }
300
301 void Window::dragEnterEvent(QDragEnterEvent *event)
302 {
303     if (event->mimeData()->hasUrls())
304     {
305         auto urls = event->mimeData()->urls();
306         if (urls.size() == 1 && urls.front().path().endsWith(".stl"))
307             event->acceptProposedAction();
308     }
309 }
310
311 void Window::dropEvent(QDropEvent *event)
312 {
313     load_stl(event->mimeData()->urls().front().toLocalFile());
314 }