#include <QMenuBar>
-#include <QMessageBox>
-#include <QFileDialog>
#include "window.h"
#include "canvas.h"
#include "loader.h"
const QString Window::RECENT_FILE_KEY = "recentFiles";
+const QString Window::INVERT_ZOOM_KEY = "invertZoom";
+const QString Window::AUTORELOAD_KEY = "autoreload";
+const QString Window::DRAW_AXES_KEY = "drawAxes";
+const QString Window::PROJECTION_KEY = "projection";
+const QString Window::DRAW_MODE_KEY = "drawMode";
+const QString Window::WINDOW_GEOM_KEY = "windowGeometry";
Window::Window(QWidget *parent) :
QMainWindow(parent),
about_action(new QAction("About", this)),
quit_action(new QAction("Quit", this)),
perspective_action(new QAction("Perspective", this)),
- orthogonal_action(new QAction("Orthographic", this)),
+ orthographic_action(new QAction("Orthographic", this)),
+ shaded_action(new QAction("Shaded", this)),
+ wireframe_action(new QAction("Wireframe", this)),
+ surfaceangle_action(new QAction("Surface Angle", this)),
+ axes_action(new QAction("Draw Axes", this)),
+ invert_zoom_action(new QAction("Invert Zoom", this)),
reload_action(new QAction("Reload", this)),
autoreload_action(new QAction("Autoreload", this)),
+ save_screenshot_action(new QAction("Save Screenshot", this)),
+ hide_menuBar_action(new QAction("Hide Menu Bar", this)),
recent_files(new QMenu("Open recent", this)),
recent_files_group(new QActionGroup(this)),
recent_files_clear_action(new QAction("Clear recent files", this)),
setWindowTitle("fstl");
setAcceptDrops(true);
- QGLFormat format;
+ QSurfaceFormat format;
+ format.setDepthBufferSize(24);
+ format.setStencilBufferSize(8);
format.setVersion(2, 1);
- format.setSampleBuffers(true);
+ format.setProfile(QSurfaceFormat::CoreProfile);
+ QSurfaceFormat::setDefaultFormat(format);
+
canvas = new Canvas(format, this);
setCentralWidget(canvas);
open_action->setShortcut(QKeySequence::Open);
QObject::connect(open_action, &QAction::triggered,
this, &Window::on_open);
+ this->addAction(open_action);
quit_action->setShortcut(QKeySequence::Quit);
QObject::connect(quit_action, &QAction::triggered,
this, &Window::close);
+ this->addAction(quit_action);
autoreload_action->setCheckable(true);
- autoreload_action->setChecked(true);
- autoreload_action->setEnabled(false);
QObject::connect(autoreload_action, &QAction::triggered,
this, &Window::on_autoreload_triggered);
QObject::connect(recent_files_group, &QActionGroup::triggered,
this, &Window::on_load_recent);
+ save_screenshot_action->setCheckable(false);
+ QObject::connect(save_screenshot_action, &QAction::triggered,
+ this, &Window::on_save_screenshot);
+
rebuild_recent_files();
auto file_menu = menuBar()->addMenu("File");
file_menu->addSeparator();
file_menu->addAction(reload_action);
file_menu->addAction(autoreload_action);
+ file_menu->addAction(save_screenshot_action);
file_menu->addAction(quit_action);
auto view_menu = menuBar()->addMenu("View");
auto projection_menu = view_menu->addMenu("Projection");
projection_menu->addAction(perspective_action);
- projection_menu->addAction(orthogonal_action);
+ projection_menu->addAction(orthographic_action);
auto projections = new QActionGroup(projection_menu);
- for (auto p : {perspective_action, orthogonal_action})
+ for (auto p : {perspective_action, orthographic_action})
{
projections->addAction(p);
p->setCheckable(true);
}
- perspective_action->setChecked(true);
projections->setExclusive(true);
QObject::connect(projections, &QActionGroup::triggered,
this, &Window::on_projection);
+ auto draw_menu = view_menu->addMenu("Draw Mode");
+ draw_menu->addAction(shaded_action);
+ draw_menu->addAction(wireframe_action);
+ draw_menu->addAction(surfaceangle_action);
+ auto drawModes = new QActionGroup(draw_menu);
+ for (auto p : {shaded_action, wireframe_action, surfaceangle_action})
+ {
+ drawModes->addAction(p);
+ p->setCheckable(true);
+ }
+ drawModes->setExclusive(true);
+ QObject::connect(drawModes, &QActionGroup::triggered,
+ this, &Window::on_drawMode);
+ view_menu->addAction(axes_action);
+ axes_action->setCheckable(true);
+ QObject::connect(axes_action, &QAction::triggered,
+ this, &Window::on_drawAxes);
+
+ view_menu->addAction(invert_zoom_action);
+ invert_zoom_action->setCheckable(true);
+ QObject::connect(invert_zoom_action, &QAction::triggered,
+ this, &Window::on_invertZoom);
+
+ view_menu->addAction(hide_menuBar_action);
+ hide_menuBar_action->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C);
+ hide_menuBar_action->setCheckable(true);
+ QObject::connect(hide_menuBar_action, &QAction::toggled,
+ this, &Window::on_hide_menuBar);
+ this->addAction(hide_menuBar_action);
+
auto help_menu = menuBar()->addMenu("Help");
help_menu->addAction(about_action);
+ load_persist_settings();
+}
+
+void Window::load_persist_settings(){
+ QSettings settings;
+ bool invert_zoom = settings.value(INVERT_ZOOM_KEY, false).toBool();
+ canvas->invert_zoom(invert_zoom);
+ invert_zoom_action->setChecked(invert_zoom);
+
+ autoreload_action->setChecked(settings.value(AUTORELOAD_KEY, true).toBool());
+
+ bool draw_axes = settings.value(DRAW_AXES_KEY, false).toBool();
+ canvas->draw_axes(draw_axes);
+ axes_action->setChecked(draw_axes);
+
+ QString projection = settings.value(PROJECTION_KEY, "perspective").toString();
+ if(projection == "perspective"){
+ canvas->view_perspective(Canvas::P_PERSPECTIVE, false);
+ perspective_action->setChecked(true);
+ }else{
+ canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, false);
+ orthographic_action->setChecked(true);
+ }
+
+ DrawMode draw_mode = (DrawMode)settings.value(DRAW_MODE_KEY, DRAWMODECOUNT).toInt();
+
+ if(draw_mode >= DRAWMODECOUNT)
+ {
+ draw_mode = shaded;
+ }
+ canvas->set_drawMode(draw_mode);
+ QAction* (dm_acts[]) = {shaded_action, wireframe_action, surfaceangle_action};
+ dm_acts[draw_mode]->setChecked(true);
+
resize(600, 400);
+ restoreGeometry(settings.value(WINDOW_GEOM_KEY).toByteArray());
}
void Window::on_open()
{
QString filename = QFileDialog::getOpenFileName(
- this, "Load .stl file", QString(), "*.stl");
- if (not filename.isNull())
+ this, "Load .stl file", QString(), "STL files (*.stl, *.STL)");
+ if (!filename.isNull())
{
load_stl(filename);
}
void Window::on_about()
{
QMessageBox::about(this, "",
- "<p align=\"center\"><b>fstl</b></p>"
+ "<p align=\"center\"><b>fstl</b><br>" FSTL_VERSION "</p>"
"<p>A fast viewer for <code>.stl</code> files.<br>"
- "<a href=\"https://github.com/mkeeter/fstl\""
- " style=\"color: #93a1a1;\">https://github.com/mkeeter/fstl</a></p>"
- "<p>© 2014-2017 Matthew Keeter<br>"
+ "<a href=\"https://github.com/fstl-app/fstl\""
+ " style=\"color: #93a1a1;\">https://github.com/fstl-app/fstl</a></p>"
+ "<p>© 2014-2022 Matthew Keeter<br>"
"<a href=\"mailto:matt.j.keeter@gmail.com\""
" style=\"color: #93a1a1;\">matt.j.keeter@gmail.com</a></p>");
}
"This file is syntactically correct<br>but contains no triangles.");
}
-void Window::on_confusing_stl()
-{
- QMessageBox::warning(this, "Warning",
- "<b>Warning:</b><br>"
- "This <code>.stl</code> file begins with <code>solid </code>but appears to be a binary file.<br>"
- "<code>fstl</code> loaded it, but other programs may be confused by this file.");
-}
-
void Window::on_missing_file()
{
QMessageBox::critical(this, "Error",
{
if (proj == perspective_action)
{
- canvas->view_perspective();
+ canvas->view_perspective(Canvas::P_PERSPECTIVE, true);
+ QSettings().setValue(PROJECTION_KEY, "perspective");
}
else
{
- canvas->view_orthographic();
+ canvas->view_perspective(Canvas::P_ORTHOGRAPHIC, true);
+ QSettings().setValue(PROJECTION_KEY, "orthographic");
}
}
+void Window::on_drawMode(QAction* act)
+{
+ DrawMode mode;
+ if (act == shaded_action)
+ {
+ mode = shaded;
+ }
+ else if (act == wireframe_action)
+ {
+ mode = wireframe;
+ }
+ else
+ {
+ mode = surfaceangle;
+ }
+ canvas->set_drawMode(mode);
+ QSettings().setValue(DRAW_MODE_KEY, mode);
+}
+
+void Window::on_drawAxes(bool d)
+{
+ canvas->draw_axes(d);
+ QSettings().setValue(DRAW_AXES_KEY, d);
+}
+
+void Window::on_invertZoom(bool d)
+{
+ canvas->invert_zoom(d);
+ QSettings().setValue(INVERT_ZOOM_KEY, d);
+}
+
void Window::on_watched_change(const QString& filename)
{
if (autoreload_action->isChecked())
{
on_reload();
}
+ QSettings().setValue(AUTORELOAD_KEY, b);
}
void Window::on_clear_recent()
load_stl(a->data().toString());
}
+void Window::on_loaded(const QString& filename)
+{
+ current_file = filename;
+}
+
+void Window::on_save_screenshot()
+{
+ const auto image = canvas->grabFramebuffer();
+ auto file_name = QFileDialog::getSaveFileName(
+ this,
+ tr("Save Screenshot Image"),
+ QStandardPaths::standardLocations(QStandardPaths::StandardLocation::PicturesLocation).first(),
+ "Images (*.png *.jpg)");
+
+ auto get_file_extension = [](const std::string& file_name) -> std::string
+ {
+ const auto location = std::find(file_name.rbegin(), file_name.rend(), '.');
+ if (location == file_name.rend())
+ {
+ return "";
+ }
+
+ const auto index = std::distance(file_name.rbegin(), location);
+ return file_name.substr(file_name.size() - index);
+ };
+
+ const auto extension = get_file_extension(file_name.toStdString());
+ if(extension.empty() || (extension != "png" && extension != "jpg"))
+ {
+ file_name.append(".png");
+ }
+
+ const auto save_ok = image.save(file_name);
+ if(!save_ok)
+ {
+ QMessageBox::warning(this, tr("Error Saving Image"), tr("Unable to save screen shot image."));
+ }
+}
+
+void Window::on_hide_menuBar()
+{
+ menuBar()->setVisible(!hide_menuBar_action->isChecked());
+}
+
void Window::rebuild_recent_files()
{
QSettings settings;
this, &Window::on_bad_stl);
connect(loader, &Loader::error_empty_mesh,
this, &Window::on_empty_mesh);
- connect(loader, &Loader::warning_confusing_stl,
- this, &Window::on_confusing_stl);
connect(loader, &Loader::error_missing_file,
this, &Window::on_missing_file);
this, &Window::setWindowTitle);
connect(loader, &Loader::loaded_file,
this, &Window::set_watched);
- autoreload_action->setEnabled(true);
+ connect(loader, &Loader::loaded_file,
+ this, &Window::on_loaded);
reload_action->setEnabled(true);
}
{
load_stl(event->mimeData()->urls().front().toLocalFile());
}
+
+void Window::resizeEvent(QResizeEvent *event)
+{
+ QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry());
+ QWidget::resizeEvent(event);
+}
+
+void Window::moveEvent(QMoveEvent *event)
+{
+ QSettings().setValue(WINDOW_GEOM_KEY, saveGeometry());
+ QWidget::moveEvent(event);
+}
+
+void Window::sorted_insert(QStringList& list, const QCollator& collator, const QString& value)
+{
+ int start = 0;
+ int end = list.size() - 1;
+ int index = 0;
+ while (start <= end){
+ int mid = (start+end)/2;
+ if (list[mid] == value) {
+ return;
+ }
+ int compare = collator.compare(value, list[mid]);
+ if (compare < 0) {
+ end = mid-1;
+ index = mid;
+ } else {
+ start = mid+1;
+ index = start;
+ }
+ }
+
+ list.insert(index, value);
+}
+
+void Window::build_folder_file_list()
+{
+ QString current_folder_path = QFileInfo(current_file).absoluteDir().absolutePath();
+ if (!lookup_folder_files.isEmpty())
+ {
+ if (current_folder_path == lookup_folder) {
+ return;
+ }
+
+ lookup_folder_files.clear();
+ }
+ lookup_folder = current_folder_path;
+
+ QCollator collator;
+ collator.setNumericMode(true);
+
+ QDirIterator dirIterator(lookup_folder, QStringList() << "*.stl", QDir::Files | QDir::Readable | QDir::Hidden);
+ while (dirIterator.hasNext()) {
+ dirIterator.next();
+
+ QString name = dirIterator.fileName();
+ sorted_insert(lookup_folder_files, collator, name);
+ }
+}
+
+QPair<QString, QString> Window::get_file_neighbors()
+{
+ if (current_file.isEmpty()) {
+ return QPair<QString, QString>(QString(), QString());
+ }
+
+ build_folder_file_list();
+
+ QFileInfo fileInfo(current_file);
+
+ QString current_dir = fileInfo.absoluteDir().absolutePath();
+ QString current_name = fileInfo.fileName();
+
+ QString prev = QString();
+ QString next = QString();
+
+ QListIterator<QString> fileIterator(lookup_folder_files);
+ while (fileIterator.hasNext()) {
+ QString name = fileIterator.next();
+
+ if (name == current_name) {
+ if (fileIterator.hasNext()) {
+ next = current_dir + QDir::separator() + fileIterator.next();
+ }
+ break;
+ }
+
+ prev = name;
+ }
+
+ if (!prev.isEmpty()) {
+ prev.prepend(QDir::separator());
+ prev.prepend(current_dir);
+ }
+
+ return QPair<QString, QString>(prev, next);
+}
+
+bool Window::load_prev(void)
+{
+ QPair<QString, QString> neighbors = get_file_neighbors();
+ if (neighbors.first.isEmpty()) {
+ return false;
+ }
+
+ return load_stl(neighbors.first);
+}
+
+bool Window::load_next(void)
+{
+ QPair<QString, QString> neighbors = get_file_neighbors();
+ if (neighbors.second.isEmpty()) {
+ return false;
+ }
+
+ return load_stl(neighbors.second);
+}
+
+void Window::keyPressEvent(QKeyEvent* event)
+{
+ if (!open_action->isEnabled())
+ {
+ QMainWindow::keyPressEvent(event);
+ return;
+ }
+
+ if (event->key() == Qt::Key_Left)
+ {
+ load_prev();
+ return;
+ }
+ else if (event->key() == Qt::Key_Right)
+ {
+ load_next();
+ return;
+ }
+ else if (event->key() == Qt::Key_Escape)
+ {
+ hide_menuBar_action->setChecked(false);
+ return;
+ }
+
+ QMainWindow::keyPressEvent(event);
+}