X-Git-Url: https://git.sur5r.net/?a=blobdiff_plain;f=src%2Fwindow.cpp;h=e9f3f94ebf2d980e1e088d876d8acf53ae89c699;hb=dd4a156f8787b310568d82161bddd478b0938499;hp=3002ecc83daa6008dd3c42c420b933c24605008e;hpb=967d178c4343c689c728571bb10d5c94ab8d9d13;p=fstl diff --git a/src/window.cpp b/src/window.cpp index 3002ecc..e9f3f94 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1,58 +1,188 @@ #include -#include -#include #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), open_action(new QAction("Open", this)), about_action(new QAction("About", this)), - quit_action(new QAction("Quit", this)) + quit_action(new QAction("Quit", this)), + perspective_action(new QAction("Perspective", 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)), + watcher(new QFileSystemWatcher(this)) { setWindowTitle("fstl"); setAcceptDrops(true); - QFile styleFile(":/qt/style.qss"); - styleFile.open( QFile::ReadOnly ); - setStyleSheet(styleFile.readAll()); - - 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); + QObject::connect(watcher, &QFileSystemWatcher::fileChanged, + this, &Window::on_watched_change); + 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); + QObject::connect(autoreload_action, &QAction::triggered, + this, &Window::on_autoreload_triggered); + + reload_action->setShortcut(QKeySequence::Refresh); + reload_action->setEnabled(false); + QObject::connect(reload_action, &QAction::triggered, + this, &Window::on_reload); QObject::connect(about_action, &QAction::triggered, this, &Window::on_about); + QObject::connect(recent_files_clear_action, &QAction::triggered, + this, &Window::on_clear_recent); + 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->addAction(open_action); + file_menu->addMenu(recent_files); + 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(orthographic_action); + auto projections = new QActionGroup(projection_menu); + for (auto p : {perspective_action, orthographic_action}) + { + projections->addAction(p); + p->setCheckable(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); } @@ -61,29 +191,35 @@ void Window::on_open() void Window::on_about() { QMessageBox::about(this, "", - "

fstl

" + "

fstl
" FSTL_VERSION "

" "

A fast viewer for .stl files.
" - "https://github.com/mkeeter/fstl

" - "

© 2014 Matthew Keeter
" + "https://github.com/fstl-app/fstl

" + "

© 2014-2022 Matthew Keeter
" "matt.j.keeter@gmail.com

"); } -void Window::on_ascii_stl() +void Window::on_bad_stl() { QMessageBox::critical(this, "Error", "Error:
" - "Cannot open ASCII .stl file
" - "Please convert to binary .stl and retry"); + "This .stl file is invalid or corrupted.
" + "Please export it from the original source, verify, and retry."); } -void Window::on_bad_stl() +void Window::on_empty_mesh() { QMessageBox::critical(this, "Error", "Error:
" - "This .stl file is invalid or corrupted.
" - "Please export it from the original source, verify, and retry."); + "This file is syntactically correct
but contains no triangles."); +} + +void Window::on_missing_file() +{ + QMessageBox::critical(this, "Error", + "Error:
" + "The target file is missing.
"); } void Window::enable_open() @@ -96,22 +232,202 @@ void Window::disable_open() open_action->setEnabled(false); } -bool Window::load_stl(const QString& filename) +void Window::set_watched(const QString& filename) +{ + const auto files = watcher->files(); + if (files.size()) + { + watcher->removePaths(watcher->files()); + } + watcher->addPath(filename); + + QSettings settings; + auto recent = settings.value(RECENT_FILE_KEY).toStringList(); + const auto f = QFileInfo(filename).absoluteFilePath(); + recent.removeAll(f); + recent.prepend(f); + while (recent.size() > MAX_RECENT_FILES) + { + recent.pop_back(); + } + settings.setValue(RECENT_FILE_KEY, recent); + rebuild_recent_files(); +} + +void Window::on_projection(QAction* proj) +{ + if (proj == perspective_action) + { + canvas->view_perspective(Canvas::P_PERSPECTIVE, true); + QSettings().setValue(PROJECTION_KEY, "perspective"); + } + else + { + 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()) + { + load_stl(filename, true); + } +} + +void Window::on_autoreload_triggered(bool b) +{ + if (b) + { + on_reload(); + } + QSettings().setValue(AUTORELOAD_KEY, b); +} + +void Window::on_clear_recent() +{ + QSettings settings; + settings.setValue(RECENT_FILE_KEY, QStringList()); + rebuild_recent_files(); +} + +void Window::on_load_recent(QAction* a) +{ + 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; + QStringList files = settings.value(RECENT_FILE_KEY).toStringList(); + + const auto actions = recent_files_group->actions(); + for (auto a : actions) + { + recent_files_group->removeAction(a); + } + recent_files->clear(); + + for (auto f : files) + { + const auto a = new QAction(f, recent_files); + a->setData(f); + recent_files_group->addAction(a); + recent_files->addAction(a); + } + if (files.size() == 0) + { + auto a = new QAction("No recent files", recent_files); + recent_files->addAction(a); + a->setEnabled(false); + } + recent_files->addSeparator(); + recent_files->addAction(recent_files_clear_action); +} + +void Window::on_reload() +{ + auto fs = watcher->files(); + if (fs.size() == 1) + { + load_stl(fs[0], true); + } +} + +bool Window::load_stl(const QString& filename, bool is_reload) { if (!open_action->isEnabled()) return false; canvas->set_status("Loading " + filename); - Loader* loader = new Loader(this, filename); + Loader* loader = new Loader(this, filename, is_reload); connect(loader, &Loader::started, this, &Window::disable_open); connect(loader, &Loader::got_mesh, canvas, &Canvas::load_mesh); - connect(loader, &Loader::error_ascii_stl, - this, &Window::on_ascii_stl); connect(loader, &Loader::error_bad_stl, this, &Window::on_bad_stl); + connect(loader, &Loader::error_empty_mesh, + this, &Window::on_empty_mesh); + connect(loader, &Loader::error_missing_file, + this, &Window::on_missing_file); connect(loader, &Loader::finished, loader, &Loader::deleteLater); @@ -124,6 +440,11 @@ bool Window::load_stl(const QString& filename) { connect(loader, &Loader::loaded_file, this, &Window::setWindowTitle); + connect(loader, &Loader::loaded_file, + this, &Window::set_watched); + connect(loader, &Loader::loaded_file, + this, &Window::on_loaded); + reload_action->setEnabled(true); } loader->start(); @@ -144,3 +465,148 @@ void Window::dropEvent(QDropEvent *event) { 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 Window::get_file_neighbors() +{ + if (current_file.isEmpty()) { + return QPair(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 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(prev, next); +} + +bool Window::load_prev(void) +{ + QPair neighbors = get_file_neighbors(); + if (neighbors.first.isEmpty()) { + return false; + } + + return load_stl(neighbors.first); +} + +bool Window::load_next(void) +{ + QPair 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); +}