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:
Dion Moult
2026-05-20 15:52:47 +10:00
parent 8734b419e5
commit 4913b84af7
7 changed files with 203 additions and 2 deletions
@@ -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