mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Add recent projects to Bonsai Viewer
Replace the "Open Recent coming soon" placeholder with a working most-recently-used project list. RecentProjects persists .ifcfed paths via QSettings, capped and pruned to existing files. The Open Recent ribbon button now shows a popup menu of recent projects; every successful open or save (local or cloud) records an entry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,8 @@ set(BONSAIVIEWER_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Commands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Commands.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/RecentProjects.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/RecentProjects.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/SaveProjectDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/SaveProjectDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.cpp
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "modules/models/View.h"
|
||||
#include "modules/models/Panel.h"
|
||||
#include "modules/project/Commands.h"
|
||||
#include "modules/project/RecentProjects.h"
|
||||
#include "modules/properties/View.h"
|
||||
#include "modules/properties/Panel.h"
|
||||
#include "modules/settings/Dialog.h"
|
||||
@@ -42,11 +43,13 @@
|
||||
#include "modules/viewport/Panel.h"
|
||||
#include "modules/viewport/View.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QDockWidget>
|
||||
#include <QFileInfo>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QShortcut>
|
||||
#include <QSignalBlocker>
|
||||
@@ -75,6 +78,13 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
connect(session_state_, &bonsaiviewer::SessionState::projectReset, this, on_clean_state);
|
||||
connect(session_state_, &bonsaiviewer::SessionState::projectOpened, this, on_clean_state);
|
||||
connect(session_state_, &bonsaiviewer::SessionState::projectSaved, this, on_clean_state);
|
||||
|
||||
// Every successful open/save (local or cloud) feeds the recent list.
|
||||
auto remember_recent = [](const QString& path) {
|
||||
modules::project::RecentProjects::add(path);
|
||||
};
|
||||
connect(session_state_, &bonsaiviewer::SessionState::projectOpened, this, remember_recent);
|
||||
connect(session_state_, &bonsaiviewer::SessionState::projectSaved, this, remember_recent);
|
||||
setupChrome();
|
||||
setupViewport();
|
||||
setupPanels();
|
||||
@@ -140,6 +150,37 @@ QToolButton* MainWindow::makePanelToggle(const QString& text, QDockWidget* dock)
|
||||
return button;
|
||||
}
|
||||
|
||||
void MainWindow::populateRecentMenu(QMenu* menu) {
|
||||
menu->clear();
|
||||
|
||||
const QStringList recent = modules::project::RecentProjects::list();
|
||||
if (recent.isEmpty()) {
|
||||
QAction* empty = menu->addAction("No Recent Projects");
|
||||
empty->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QString& path : recent) {
|
||||
QAction* action = menu->addAction(QFileInfo(path).fileName());
|
||||
action->setToolTip(path);
|
||||
connect(action, &QAction::triggered, this, [this, path]() {
|
||||
const bool opened = modules::project::commands::openProjectPath(
|
||||
*session_state_, *this, *viewport_widget_->viewport(), path);
|
||||
// A recent entry that no longer loads is dropped so it stops
|
||||
// cluttering the menu (e.g. moved/deleted file).
|
||||
if (!opened && !QFileInfo::exists(path)) {
|
||||
modules::project::RecentProjects::remove(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
QAction* clear = menu->addAction("Clear Recent Projects");
|
||||
connect(clear, &QAction::triggered, this, []() {
|
||||
modules::project::RecentProjects::clear();
|
||||
});
|
||||
}
|
||||
|
||||
QWidget* MainWindow::buildHomeRibbonPage() {
|
||||
auto* page = new QFrame(this);
|
||||
page->setObjectName("ribbonPage");
|
||||
@@ -163,8 +204,12 @@ QWidget* MainWindow::buildHomeRibbonPage() {
|
||||
*session_state_, *this, *viewport_widget_->viewport());
|
||||
});
|
||||
auto* open_recent = components::buttons::makeButton("Open Recent", ":/icons/clock-rotate-right.svg", this);
|
||||
connect(open_recent, &QToolButton::clicked, this, [this]() {
|
||||
session_state_->setStatusMessage("Project", "Open Recent coming soon");
|
||||
auto* recent_menu = new QMenu(open_recent);
|
||||
recent_menu->setToolTipsVisible(true);
|
||||
open_recent->setMenu(recent_menu);
|
||||
open_recent->setPopupMode(QToolButton::InstantPopup);
|
||||
connect(recent_menu, &QMenu::aboutToShow, this, [this, recent_menu]() {
|
||||
populateRecentMenu(recent_menu);
|
||||
});
|
||||
auto* save_project = components::buttons::makeButton("Save Project", ":/icons/floppy-disk.svg", this);
|
||||
connect(save_project, &QToolButton::clicked, this, [this]() {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
class QLabel;
|
||||
class QDockWidget;
|
||||
class QMenu;
|
||||
class QProgressBar;
|
||||
class QStackedWidget;
|
||||
class QToolButton;
|
||||
@@ -61,6 +62,9 @@ private:
|
||||
QWidget* buildPanelsRibbonPage();
|
||||
void updateWindowTitle();
|
||||
QToolButton* makePanelToggle(const QString& text, QDockWidget* dock);
|
||||
// Rebuilds `menu` from the persisted recent-projects list. Wired to the
|
||||
// menu's aboutToShow so it always reflects the current MRU state.
|
||||
void populateRecentMenu(QMenu* menu);
|
||||
|
||||
private:
|
||||
QLabel* status_mode_label_ = nullptr;
|
||||
|
||||
@@ -297,6 +297,11 @@ bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
|
||||
return openProjectAt(s, host, vp, path);
|
||||
}
|
||||
|
||||
bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) {
|
||||
if (path.isEmpty()) return false;
|
||||
return openProjectAt(s, host, vp, path);
|
||||
}
|
||||
|
||||
bool openCloudProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
|
||||
SceneLoader* loader = s.loader();
|
||||
if (loader && loader->isLoading()) {
|
||||
|
||||
@@ -34,6 +34,10 @@ namespace bonsaiviewer::modules::project::commands {
|
||||
// projectSaved) so views refresh once per command.
|
||||
bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp);
|
||||
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp);
|
||||
// Open a specific .ifcfed by path, bypassing the file dialog. Used by the
|
||||
// "Open Recent" menu. Same dirty-check / load / cloud-resolve flow as
|
||||
// openProject; returns false if the load failed or was cancelled.
|
||||
bool openProjectPath(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path);
|
||||
// Pick a connector, then call pull_ifcfed_interactive and open the resulting
|
||||
// .ifcfed as a fresh project. Non-local models in the loaded federation are
|
||||
// resolved asynchronously via pull_models.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "RecentProjects.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
namespace bonsaiviewer::modules::project {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kRecentKey = "project/recent_projects";
|
||||
|
||||
// Absolute, cleaned form used both for storage and de-duplication.
|
||||
QString canonicalize(const QString& path) {
|
||||
if (path.isEmpty()) return {};
|
||||
return QFileInfo(path).absoluteFilePath();
|
||||
}
|
||||
|
||||
QStringList readRaw() {
|
||||
QSettings settings;
|
||||
return settings.value(kRecentKey).toStringList();
|
||||
}
|
||||
|
||||
void writeRaw(const QStringList& paths) {
|
||||
QSettings settings;
|
||||
settings.setValue(kRecentKey, paths);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QStringList RecentProjects::list() {
|
||||
const QStringList stored = readRaw();
|
||||
QStringList existing;
|
||||
for (const QString& path : stored) {
|
||||
if (QFileInfo::exists(path)) existing << path;
|
||||
}
|
||||
// Persist the pruned list so vanished files don't linger forever.
|
||||
if (existing != stored) writeRaw(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
void RecentProjects::add(const QString& path) {
|
||||
const QString canonical = canonicalize(path);
|
||||
if (canonical.isEmpty()) return;
|
||||
|
||||
QStringList paths = readRaw();
|
||||
paths.removeAll(canonical);
|
||||
paths.prepend(canonical);
|
||||
while (paths.size() > kMaxEntries) paths.removeLast();
|
||||
writeRaw(paths);
|
||||
}
|
||||
|
||||
void RecentProjects::remove(const QString& path) {
|
||||
const QString canonical = canonicalize(path);
|
||||
if (canonical.isEmpty()) return;
|
||||
|
||||
QStringList paths = readRaw();
|
||||
if (paths.removeAll(canonical) > 0) writeRaw(paths);
|
||||
}
|
||||
|
||||
void RecentProjects::clear() {
|
||||
QSettings settings;
|
||||
settings.remove(kRecentKey);
|
||||
}
|
||||
|
||||
} // namespace bonsaiviewer::modules::project
|
||||
@@ -0,0 +1,56 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_MODULES_PROJECT_RECENTPROJECTS_H
|
||||
#define IFCINTERFACE_MODULES_PROJECT_RECENTPROJECTS_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace bonsaiviewer::modules::project {
|
||||
|
||||
// Persistent most-recently-used list of project (.ifcfed) paths, backed by
|
||||
// QSettings so it survives across sessions. All entries are stored as
|
||||
// absolute, cleaned paths; the list is capped and de-duplicated.
|
||||
class RecentProjects {
|
||||
public:
|
||||
// Hard cap on how many entries are kept / shown.
|
||||
static constexpr int kMaxEntries = 10;
|
||||
|
||||
// Recent project paths, most-recent first. Entries whose file no longer
|
||||
// exists on disk are pruned (and the pruned list is persisted) so callers
|
||||
// never need to filter the result themselves.
|
||||
static QStringList list();
|
||||
|
||||
// Promote `path` to the front of the list. De-duplicates against any
|
||||
// existing entry for the same file and trims the list to kMaxEntries.
|
||||
// A no-op for empty paths.
|
||||
static void add(const QString& path);
|
||||
|
||||
// Drop a single entry, e.g. after a failed open. No-op if absent.
|
||||
static void remove(const QString& path);
|
||||
|
||||
// Forget every entry.
|
||||
static void clear();
|
||||
};
|
||||
|
||||
} // namespace bonsaiviewer::modules::project
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user