Interface mockup 13

This commit is contained in:
Dion Moult
2026-05-09 07:11:58 +10:00
parent e864a961ee
commit 027e2406be
12 changed files with 399 additions and 394 deletions
+2
View File
@@ -63,6 +63,8 @@ set(INTERFACE_FILES
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/Widget.h
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/View.cpp
${CMAKE_CURRENT_SOURCE_DIR}/panels/properties/View.h
${CMAKE_CURRENT_SOURCE_DIR}/panels/project/Controller.cpp
${CMAKE_CURRENT_SOURCE_DIR}/panels/project/Controller.h
${CMAKE_CURRENT_SOURCE_DIR}/panels/settings/Dialog.cpp
${CMAKE_CURRENT_SOURCE_DIR}/panels/settings/Dialog.h
${CMAKE_CURRENT_SOURCE_DIR}/panels/spatial_hierarchy/Types.h
+77 -359
View File
@@ -30,11 +30,11 @@
#include "components/Panel.h"
#include "components/Style.h"
#include "components/Tabs.h"
#include "panels/add_model/Dialog.h"
#include "panels/models/Controller.h"
#include "panels/todo/Widget.h"
#include "panels/models/View.h"
#include "panels/models/Widget.h"
#include "panels/project/Controller.h"
#include "panels/properties/View.h"
#include "panels/properties/Widget.h"
#include "panels/settings/Dialog.h"
@@ -44,17 +44,14 @@
#include "panels/viewport/Widget.h"
#include <QDockWidget>
#include <QFileDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QListView>
#include <QMessageBox>
#include <QSignalBlocker>
#include <QStackedWidget>
#include <QStatusBar>
#include <QTreeView>
#include <QToolButton>
#include <QVBoxLayout>
@@ -72,6 +69,14 @@ MainWindow::MainWindow(QWidget* parent)
setWindowModified(dirty);
updateWindowTitle();
});
connect(session_state_, &ifcinterface::SessionState::modelsChanged,
this, &MainWindow::updateWindowTitle);
connect(session_state_, &ifcinterface::SessionState::projectOpened,
this, [this](const QString&) { updateWindowTitle(); });
connect(session_state_, &ifcinterface::SessionState::projectSaved,
this, [this](const QString&) { updateWindowTitle(); });
connect(session_state_, &ifcinterface::SessionState::projectReset,
this, &MainWindow::updateWindowTitle);
setupChrome();
setupViewport();
setupPanels();
@@ -89,16 +94,8 @@ void MainWindow::setupChrome() {
updateWindowTitle();
}
QToolButton* MainWindow::makeRibbonAction(const QString& text, const QString& icon_path) {
return components::buttons::makeButton(text, icon_path, this, QSize(90, 54));
}
QWidget* MainWindow::makeRibbonGroup(const QString& title, const QList<QToolButton*>& buttons) {
return components::buttons::makeButtonGroup(title, buttons, this);
}
QToolButton* MainWindow::makePanelToggle(const QString& text, QDockWidget* dock) {
auto* button = makeRibbonAction(text, ":/icons/sidebar-expand.svg");
auto* button = components::buttons::makeButton(text, ":/icons/sidebar-expand.svg", this);
button->setCheckable(true);
button->setChecked(dock->isVisible());
connect(button, &QToolButton::toggled, dock, [dock](bool checked) {
@@ -119,39 +116,49 @@ QWidget* MainWindow::buildHomeRibbonPage() {
row->setContentsMargins(6, 4, 6, 4);
row->setSpacing(0);
auto* new_project = makeRibbonAction("New Project", ":/icons/plus-square.svg");
connect(new_project, &QToolButton::clicked, this, &MainWindow::onNewProject);
auto* open_project = makeRibbonAction("Open Project", ":/icons/download-square.svg");
connect(open_project, &QToolButton::clicked, this, &MainWindow::onOpenProject);
auto* open_cloud = makeRibbonAction("Open Cloud", ":/icons/cloud-square.svg");
auto* new_project = components::buttons::makeButton("New Project", ":/icons/plus-square.svg", this);
connect(new_project, &QToolButton::clicked, this, [this]() {
project_controller_->newProject();
});
auto* open_project = components::buttons::makeButton("Open Project", ":/icons/download-square.svg", this);
connect(open_project, &QToolButton::clicked, this, [this]() {
project_controller_->openProject();
});
auto* open_cloud = components::buttons::makeButton("Open Cloud", ":/icons/cloud-square.svg", this);
connect(open_cloud, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Project", "Open Cloud Project coming soon");
});
auto* open_recent = makeRibbonAction("Open Recent", ":/icons/clock-rotate-right.svg");
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* save_project = makeRibbonAction("Save Project", ":/icons/floppy-disk.svg");
connect(save_project, &QToolButton::clicked, this, &MainWindow::onSaveProject);
auto* save_project_as = makeRibbonAction("Save As", ":/icons/floppy-disk-arrow-in.svg");
connect(save_project_as, &QToolButton::clicked, this, &MainWindow::onSaveProjectAs);
auto* save_project = components::buttons::makeButton("Save Project", ":/icons/floppy-disk.svg", this);
connect(save_project, &QToolButton::clicked, this, [this]() {
project_controller_->saveProject();
});
auto* save_project_as = components::buttons::makeButton("Save As", ":/icons/floppy-disk-arrow-in.svg", this);
connect(save_project_as, &QToolButton::clicked, this, [this]() {
project_controller_->saveProjectAs();
});
auto* add_model = makeRibbonAction("Add Model", ":/icons/cube.svg");
connect(add_model, &QToolButton::clicked, this, &MainWindow::onAddFiles);
auto* sync_models = makeRibbonAction("Sync Models", ":/icons/refresh-double.svg");
auto* add_model = components::buttons::makeButton("Add Model", ":/icons/cube.svg", this);
connect(add_model, &QToolButton::clicked, this, [this]() {
models_controller_->addFiles();
});
auto* sync_models = components::buttons::makeButton("Sync Models", ":/icons/refresh-double.svg", this);
connect(sync_models, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Models", "Sync models coming soon");
});
auto* settings_button = makeRibbonAction("Settings", ":/icons/settings.svg");
auto* settings_button = components::buttons::makeButton("Settings", ":/icons/settings.svg", this);
connect(settings_button, &QToolButton::clicked, this, [this]() {
panels::settings::SettingsDialog dialog(this);
dialog.exec();
});
row->addWidget(makeRibbonGroup("PROJECT", {new_project, open_project, open_cloud, open_recent, save_project, save_project_as}));
row->addWidget(makeRibbonGroup("MODELS", {add_model, sync_models}));
row->addWidget(makeRibbonGroup("SETTINGS", {settings_button}));
row->addWidget(components::buttons::makeButtonGroup("PROJECT", {new_project, open_project, open_cloud, open_recent, save_project, save_project_as}, this));
row->addWidget(components::buttons::makeButtonGroup("MODELS", {add_model, sync_models}, this));
row->addWidget(components::buttons::makeButtonGroup("SETTINGS", {settings_button}, this));
row->addStretch(1);
return page;
}
@@ -163,36 +170,40 @@ QWidget* MainWindow::buildNavigateRibbonPage() {
row->setContentsMargins(2, 4, 2, 4);
row->setSpacing(0);
auto* set_home = makeRibbonAction("Set Home", ":/icons/home.svg");
connect(set_home, &QToolButton::clicked, this, &MainWindow::onSetHomeView);
auto* go_home = makeRibbonAction("Go Home", ":/icons/home-alt.svg");
connect(go_home, &QToolButton::clicked, this, &MainWindow::onGoHomeView);
auto* view_all = makeRibbonAction("View All", ":/icons/cube-scan.svg");
auto* set_home = components::buttons::makeButton("Set Home", ":/icons/home.svg", this);
connect(set_home, &QToolButton::clicked, this, [this]() {
viewport_controller_->setHomeView();
});
auto* go_home = components::buttons::makeButton("Go Home", ":/icons/home-alt.svg", this);
connect(go_home, &QToolButton::clicked, this, [this]() {
viewport_controller_->goHomeView();
});
auto* view_all = components::buttons::makeButton("View All", ":/icons/cube-scan.svg", this);
connect(view_all, &QToolButton::clicked, this, [this]() {
if (viewport_widget_) viewport_widget_->viewport()->viewAll();
});
auto* view_selected = makeRibbonAction("View Selected", ":/icons/cube-scan-solid.svg");
auto* view_selected = components::buttons::makeButton("View Selected", ":/icons/cube-scan-solid.svg", this);
connect(view_selected, &QToolButton::clicked, this, [this]() {
if (viewport_widget_) viewport_widget_->viewport()->focusOnSelectedObject();
});
auto* plan_view = makeRibbonAction("Plan", ":/icons/planimetry.svg");
auto* plan_view = components::buttons::makeButton("Plan", ":/icons/planimetry.svg", this);
connect(plan_view, &QToolButton::clicked, this, [this]() {
if (viewport_widget_) viewport_widget_->viewport()->setStandardView(90.0f, 90.0f);
});
auto* front_view = makeRibbonAction("Front", ":/icons/city.svg");
auto* front_view = components::buttons::makeButton("Front", ":/icons/city.svg", this);
connect(front_view, &QToolButton::clicked, this, [this]() {
if (viewport_widget_) viewport_widget_->viewport()->setStandardView(0.0f, 0.0f);
});
auto* side_view = makeRibbonAction("Side", ":/icons/building.svg");
auto* side_view = components::buttons::makeButton("Side", ":/icons/building.svg", this);
connect(side_view, &QToolButton::clicked, this, [this]() {
if (viewport_widget_) viewport_widget_->viewport()->setStandardView(90.0f, 0.0f);
});
auto* align_object = makeRibbonAction("Align Object", ":/icons/cellar.svg");
auto* align_object = components::buttons::makeButton("Align Object", ":/icons/cellar.svg", this);
connect(align_object, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Orientation", "Align to object coming soon");
});
auto* projection_button = makeRibbonAction("Perspective", ":/icons/perspective-view.svg");
auto* projection_button = components::buttons::makeButton("Perspective", ":/icons/perspective-view.svg", this);
connect(projection_button, &QToolButton::clicked, this, [this, projection_button]() {
if (!viewport_widget_) return;
viewport_widget_->viewport()->toggleProjection();
@@ -200,18 +211,18 @@ QWidget* MainWindow::buildNavigateRibbonPage() {
viewport_widget_->viewport()->projectionOrtho() ? "Ortho" : "Perspective");
});
auto* orbit_mode = makeRibbonAction("Orbit", ":/icons/rotate-camera-right.svg");
auto* orbit_mode = components::buttons::makeButton("Orbit", ":/icons/rotate-camera-right.svg", this);
connect(orbit_mode, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Mode", "Orbit mode active");
});
auto* fly_mode = makeRibbonAction("Fly", ":/icons/drone.svg");
auto* fly_mode = components::buttons::makeButton("Fly", ":/icons/drone.svg", this);
connect(fly_mode, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Mode", "Fly mode coming soon");
});
row->addWidget(makeRibbonGroup("CAMERA", {set_home, go_home, view_all, view_selected}));
row->addWidget(makeRibbonGroup("ORIENTATION", {plan_view, front_view, side_view, align_object, projection_button}));
row->addWidget(makeRibbonGroup("MODE", {orbit_mode, fly_mode}));
row->addWidget(components::buttons::makeButtonGroup("CAMERA", {set_home, go_home, view_all, view_selected}, this));
row->addWidget(components::buttons::makeButtonGroup("ORIENTATION", {plan_view, front_view, side_view, align_object, projection_button}, this));
row->addWidget(components::buttons::makeButtonGroup("MODE", {orbit_mode, fly_mode}, this));
row->addStretch(1);
return page;
}
@@ -223,38 +234,38 @@ QWidget* MainWindow::buildInspectRibbonPage() {
row->setContentsMargins(2, 4, 2, 4);
row->setSpacing(0);
auto* hide_selected = makeRibbonAction("Hide", ":/icons/eye-closed.svg");
auto* hide_selected = components::buttons::makeButton("Hide", ":/icons/eye-closed.svg", this);
connect(hide_selected, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Selection", "Hide selected coming soon");
});
auto* isolate_selected = makeRibbonAction("Isolate", ":/icons/eye-solid.svg");
auto* isolate_selected = components::buttons::makeButton("Isolate", ":/icons/eye-solid.svg", this);
connect(isolate_selected, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Selection", "Isolate selected coming soon");
});
auto* show_all = makeRibbonAction("Show All", ":/icons/eye.svg");
auto* show_all = components::buttons::makeButton("Show All", ":/icons/eye.svg", this);
connect(show_all, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Selection", "Show all coming soon");
});
auto* invert_selection = makeRibbonAction("Invert", ":/icons/intersect.svg");
auto* invert_selection = components::buttons::makeButton("Invert", ":/icons/intersect.svg", this);
connect(invert_selection, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Selection", "Invert selection coming soon");
});
auto* distance = makeRibbonAction("Distance", ":/icons/select-edge3d.svg");
auto* distance = components::buttons::makeButton("Distance", ":/icons/select-edge3d.svg", this);
connect(distance, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Measure", "Distance coming soon");
});
auto* area = makeRibbonAction("Area", ":/icons/select-face3d.svg");
auto* area = components::buttons::makeButton("Area", ":/icons/select-face3d.svg", this);
connect(area, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Measure", "Area coming soon");
});
auto* volume = makeRibbonAction("Volume", ":/icons/select-point3d.svg");
auto* volume = components::buttons::makeButton("Volume", ":/icons/select-point3d.svg", this);
connect(volume, &QToolButton::clicked, this, [this]() {
session_state_->setStatusMessage("Measure", "Volume coming soon");
});
row->addWidget(makeRibbonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}));
row->addWidget(makeRibbonGroup("MEASURE", {distance, area, volume}));
row->addWidget(components::buttons::makeButtonGroup("SELECTION", {hide_selected, isolate_selected, show_all, invert_selection}, this));
row->addWidget(components::buttons::makeButtonGroup("MEASURE", {distance, area, volume}, this));
row->addStretch(1);
return page;
}
@@ -266,22 +277,22 @@ QWidget* MainWindow::buildPanelsRibbonPage() {
row->setContentsMargins(2, 4, 2, 4);
row->setSpacing(0);
row->addWidget(makeRibbonGroup("DATA", {
row->addWidget(components::buttons::makeButtonGroup("DATA", {
makePanelToggle("Models", models_panel_),
makePanelToggle("Spatial", spatial_panel_),
makePanelToggle("Layers", layers_panel_),
makePanelToggle("Properties", properties_panel_)
}));
row->addWidget(makeRibbonGroup("QUERY", {
}, this));
row->addWidget(components::buttons::makeButtonGroup("QUERY", {
makePanelToggle("Views", stored_views_panel_),
makePanelToggle("Search", search_panel_),
makePanelToggle("Sheets", spreadsheet_panel_),
makePanelToggle("Audit", audit_panel_)
}));
row->addWidget(makeRibbonGroup("COLLABORATE", {
}, this));
row->addWidget(components::buttons::makeButtonGroup("COLLABORATE", {
makePanelToggle("Clash", clash_panel_),
makePanelToggle("Issues", issues_panel_)
}));
}, this));
row->addStretch(1);
return page;
}
@@ -334,7 +345,7 @@ void MainWindow::setupPanels() {
properties_panel_ = new panels::properties::PropertiesPanelWidget(this);
models_controller_ = new panels::models::ModelsPanelController(
models_panel_, session_state_, viewport_widget_->viewport(), element_registry_, this);
this, models_panel_, session_state_, viewport_widget_->viewport(), element_registry_, this);
models_view_ = new panels::models::ModelsPanelView(models_panel_, session_state_, this);
spatial_view_ = new panels::spatial_hierarchy::SpatialHierarchyPanelView(spatial_panel_, session_state_, this);
properties_view_ = new panels::properties::PropertiesPanelView(properties_panel_, session_state_, this);
@@ -411,14 +422,12 @@ void MainWindow::setupLoader() {
loader_ = new SceneLoader(viewport_widget_->viewport(), this);
element_registry_->bindLoader(loader_);
session_state_->bindLoader(loader_);
models_controller_->bindLoader(loader_);
viewport_controller_ = new panels::viewport::ViewportController(
session_state_, viewport_widget_->viewport(), this);
connect(loader_, &SceneLoader::loadStarted, this, &MainWindow::onLoadStarted);
connect(loader_, &SceneLoader::loadedFromSidecar, this, &MainWindow::onLoadedFromSidecar);
connect(loader_, &SceneLoader::loadedFromStream, this, &MainWindow::onLoadedFromStream);
connect(loader_, &SceneLoader::loadCancelled, this, &MainWindow::onLoadCancelled);
connect(loader_, &SceneLoader::loadError, this, &MainWindow::onLoadError);
connect(loader_, &SceneLoader::allLoadsFinished, this, &MainWindow::onAllLoadsFinished);
project_controller_ = new panels::project::ProjectController(
this, federation_, session_state_, element_registry_,
viewport_widget_->viewport(), models_controller_, viewport_controller_, this);
connect(viewport_widget_->viewport(), &ViewportWindow::frameStatsUpdated, this,
[this](const ViewportWindow::FrameStats& s) {
@@ -440,204 +449,6 @@ void MainWindow::setupLoader() {
});
}
void MainWindow::addFiles(const QStringList& paths) {
QStringList accepted_paths;
QStringList accepted_fed_ids;
for (const auto& path : paths) {
const QString fed_id = federation_->addModel(path);
if (fed_id.isEmpty()) continue;
accepted_paths << path;
accepted_fed_ids << fed_id;
}
loadModelsFromPaths(accepted_paths, accepted_fed_ids);
updateWindowTitle();
}
QString MainWindow::formatElapsed(qint64 ms) const {
return (ms >= 1000)
? QString::number(ms / 1000.0, 'f', 2) + " s"
: QString::number(ms) + " ms";
}
void MainWindow::onAddFiles() {
panels::add_model::AddModelDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) return;
QStringList paths;
switch (dialog.selectedMode()) {
case panels::add_model::SourceMode::IfcFile: {
QFileDialog file_dialog(this, "Add IFC Files");
file_dialog.setFileMode(QFileDialog::ExistingFiles);
file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)");
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (file_dialog.exec() == QDialog::Accepted) {
paths = file_dialog.selectedFiles();
}
break;
}
case panels::add_model::SourceMode::IfcDatabase: {
QFileDialog database_dialog(this, "Add IFC Databases");
database_dialog.setFileMode(QFileDialog::Directory);
database_dialog.setOption(QFileDialog::ShowDirsOnly, true);
database_dialog.setOption(QFileDialog::DontResolveSymlinks, true);
database_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (auto* list = database_dialog.findChild<QListView*>("listView")) {
list->setSelectionMode(QAbstractItemView::ExtendedSelection);
}
if (auto* tree = database_dialog.findChild<QTreeView*>()) {
tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
}
if (database_dialog.exec() == QDialog::Accepted) {
paths = database_dialog.selectedFiles();
}
break;
}
case panels::add_model::SourceMode::GeometryOnly: {
QFileDialog file_dialog(this, "Add Geometry Only");
file_dialog.setFileMode(QFileDialog::ExistingFiles);
file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)");
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (file_dialog.exec() == QDialog::Accepted) {
paths = file_dialog.selectedFiles();
}
break;
}
case panels::add_model::SourceMode::None:
return;
}
addFiles(paths);
}
void MainWindow::clearScene() {
if (viewport_widget_) viewport_widget_->viewport()->setSelectedObjectId(0);
session_state_->setSelectedObjectId(0);
session_state_->notifySelectionChanged();
const auto model_ids = session_state_->modelIds();
for (uint32_t mid : model_ids) {
viewport_widget_->viewport()->removeModel(mid);
loader_->removeModel(mid);
}
session_state_->clearModelMappings();
element_registry_->clear();
session_state_->notifyModelsChanged();
}
bool MainWindow::confirmDiscardIfDirty() {
if (!federation_->isDirty()) return true;
const auto result = QMessageBox::question(
this, "Unsaved Project",
"The current project has unsaved changes. Save before continuing?",
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
QMessageBox::Save);
if (result == QMessageBox::Cancel) return false;
if (result == QMessageBox::Save) return saveProject();
return true;
}
void MainWindow::loadModelsFromPaths(const QStringList& paths, const QStringList& fed_ids) {
if (paths.isEmpty()) return;
const auto ids = loader_->addFiles(paths);
for (int i = 0; i < paths.size() && i < static_cast<int>(ids.size()) && i < fed_ids.size(); ++i) {
session_state_->setModelMapping(fed_ids[i], ids[i]);
}
session_state_->notifyModelsChanged();
}
bool MainWindow::openProject(const QString& path) {
if (loader_ && loader_->isLoading()) {
QMessageBox::information(
this, "Open Project",
"Wait until the current model load finishes before opening another project.");
return false;
}
if (!confirmDiscardIfDirty()) return false;
QStringList warnings;
QString err;
if (!federation_->load(path, &warnings, &err)) {
QMessageBox::warning(this, "Open Project",
QString("Could not open project:\n%1").arg(err));
return false;
}
clearScene();
QStringList paths;
QStringList fed_ids;
QStringList missing;
for (const auto& model : federation_->models()) {
if (model.source_kind != "local") continue;
if (!QFileInfo::exists(model.source_path)) {
missing << model.source_path;
continue;
}
paths << model.source_path;
fed_ids << model.id;
}
loadModelsFromPaths(paths, fed_ids);
for (const auto& msg : missing) {
warnings << QString("Source not found, kept in project: %1").arg(msg);
}
if (!warnings.isEmpty()) {
QMessageBox::warning(this, "Open Project",
"Project opened with warnings:\n\n" + warnings.join("\n"));
}
federation_->markClean();
viewport_controller_->applyFederatedFalseOrigin();
if (federation_->hasHomeView()) {
const auto& hv = federation_->homeView();
viewport_widget_->viewport()->setCamera(
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
}
updateWindowTitle();
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
session_state_->notifyProjectOpened(path);
return true;
}
bool MainWindow::saveProject() {
if (federation_->filePath().isEmpty()) return saveProjectAs();
QString err;
if (!federation_->save(federation_->filePath(), &err)) {
QMessageBox::warning(this, "Save Project",
QString("Could not save project:\n%1").arg(err));
return false;
}
updateWindowTitle();
session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName());
return true;
}
bool MainWindow::saveProjectAs() {
QString suggested = federation_->filePath();
if (suggested.isEmpty()) suggested = "project.ifcfed";
QFileDialog file_dialog(this, "Save Project As", suggested);
file_dialog.setAcceptMode(QFileDialog::AcceptSave);
file_dialog.setFileMode(QFileDialog::AnyFile);
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (file_dialog.exec() != QDialog::Accepted) return false;
QString path = file_dialog.selectedFiles().value(0);
if (path.isEmpty()) return false;
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
QString err;
if (!federation_->save(path, &err)) {
QMessageBox::warning(this, "Save Project",
QString("Could not save project:\n%1").arg(err));
return false;
}
updateWindowTitle();
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
return true;
}
void MainWindow::updateWindowTitle() {
const QString project_path = federation_ ? federation_->filePath() : QString();
if (project_path.isEmpty() && (!federation_ || federation_->models().empty())) {
@@ -649,97 +460,4 @@ void MainWindow::updateWindowTitle() {
}
}
void MainWindow::onNewProject() {
if (loader_ && loader_->isLoading()) {
QMessageBox::information(
this, "New Project",
"Wait until the current model load finishes before creating a new project.");
return;
}
if (!confirmDiscardIfDirty()) return;
clearScene();
federation_->clear();
viewport_controller_->applyFederatedFalseOrigin();
updateWindowTitle();
session_state_->setStatusMessage("Project", "Untitled");
session_state_->notifyProjectReset();
}
void MainWindow::onOpenProject() {
QFileDialog file_dialog(this, "Open Project");
file_dialog.setFileMode(QFileDialog::ExistingFile);
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (file_dialog.exec() != QDialog::Accepted) return;
const QString path = file_dialog.selectedFiles().value(0);
if (path.isEmpty()) return;
openProject(path);
}
void MainWindow::onSaveProject() {
saveProject();
}
void MainWindow::onSaveProjectAs() {
saveProjectAs();
}
void MainWindow::onSetHomeView() {
auto camera = viewport_widget_->viewport()->cameraState();
Federation::HomeView home_view;
home_view.target = camera.target;
home_view.distance = camera.distance;
home_view.yaw = camera.yaw;
home_view.pitch = camera.pitch;
federation_->setHomeView(home_view);
updateWindowTitle();
session_state_->setStatusMessage("Camera", "Home view updated");
}
void MainWindow::onGoHomeView() {
if (!federation_->hasHomeView()) {
session_state_->setStatusMessage("Camera", "No home view set for this project");
return;
}
const auto& home_view = federation_->homeView();
viewport_widget_->viewport()->setCamera(
home_view.target.x(), home_view.target.y(), home_view.target.z(),
home_view.distance, home_view.yaw, home_view.pitch);
session_state_->setStatusMessage("Camera", "Home view restored");
}
void MainWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) {
session_state_->setStatusMessage("Loading", display_name);
}
void MainWindow::onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms) {
session_state_->setStatusMessage(
"Loaded",
QString("%1 from cache in %2")
.arg(loader_->displayName(mid))
.arg(formatElapsed(elapsed_ms)));
}
void MainWindow::onLoadedFromStream(uint32_t mid, qint64 elapsed_ms) {
session_state_->setStatusMessage(
"Loaded",
QString("%1 streamed in %2")
.arg(loader_->displayName(mid))
.arg(formatElapsed(elapsed_ms)));
}
void MainWindow::onLoadCancelled(uint32_t mid) {
session_state_->setStatusMessage("Cancelled", loader_->displayName(mid));
}
void MainWindow::onLoadError(uint32_t /*mid*/, QString message) {
session_state_->setStatusMessage("Error", message);
QMessageBox::warning(this, "IfcInterfaceMockup", message);
}
void MainWindow::onAllLoadsFinished() {
session_state_->setStatusMessage("Loaded", QString("%1 model(s)").arg(loader_->modelCount()));
}
} // namespace ifcinterface::shell
+2 -25
View File
@@ -37,6 +37,7 @@ namespace ifcinterface::components { class TabBar; }
namespace ifcinterface::panels::models { class ModelsPanelController; }
namespace ifcinterface::panels::models { class ModelsPanelWidget; }
namespace ifcinterface::panels::models { class ModelsPanelView; }
namespace ifcinterface::panels::project { class ProjectController; }
namespace ifcinterface::panels::spatial_hierarchy { class SpatialHierarchyPanelWidget; }
namespace ifcinterface::panels::spatial_hierarchy { class SpatialHierarchyPanelView; }
namespace ifcinterface::panels::properties { class PropertiesPanelWidget; }
@@ -62,33 +63,8 @@ private:
QWidget* buildNavigateRibbonPage();
QWidget* buildInspectRibbonPage();
QWidget* buildPanelsRibbonPage();
void clearScene();
bool confirmDiscardIfDirty();
void loadModelsFromPaths(const QStringList& paths, const QStringList& fed_ids);
bool openProject(const QString& path);
bool saveProject();
bool saveProjectAs();
void updateWindowTitle();
QToolButton* makeRibbonAction(const QString& text, const QString& icon_path);
QWidget* makeRibbonGroup(const QString& title, const QList<QToolButton*>& buttons);
QToolButton* makePanelToggle(const QString& text, QDockWidget* dock);
void addFiles(const QStringList& paths);
QString formatElapsed(qint64 ms) const;
private slots:
void onAddFiles();
void onLoadStarted(uint32_t mid, QString display_name);
void onLoadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
void onLoadedFromStream(uint32_t mid, qint64 elapsed_ms);
void onLoadCancelled(uint32_t mid);
void onLoadError(uint32_t mid, QString message);
void onAllLoadsFinished();
void onSetHomeView();
void onGoHomeView();
void onNewProject();
void onOpenProject();
void onSaveProject();
void onSaveProjectAs();
private:
Federation* federation_ = nullptr;
@@ -114,6 +90,7 @@ private:
QDockWidget* issues_panel_ = nullptr;
ifcinterface::panels::models::ModelsPanelController* models_controller_ = nullptr;
ifcinterface::panels::models::ModelsPanelView* models_view_ = nullptr;
ifcinterface::panels::project::ProjectController* project_controller_ = nullptr;
ifcinterface::panels::spatial_hierarchy::SpatialHierarchyPanelView* spatial_view_ = nullptr;
ifcinterface::panels::properties::PropertiesPanelView* properties_view_ = nullptr;
};
+4
View File
@@ -100,6 +100,10 @@ void SessionState::notifyProjectOpened(const QString& path) {
emit projectOpened(path);
}
void SessionState::notifyProjectSaved(const QString& path) {
emit projectSaved(path);
}
void SessionState::notifyProjectReset() {
emit projectReset();
}
+2
View File
@@ -64,10 +64,12 @@ public:
void notifyFederationStructureChanged();
void notifyVisibilityChanged();
void notifyProjectOpened(const QString& path);
void notifyProjectSaved(const QString& path);
void notifyProjectReset();
signals:
void projectOpened(const QString& path);
void projectSaved(const QString& path);
void projectReset();
void modelsChanged();
void federationStructureChanged();
+2 -3
View File
@@ -32,15 +32,14 @@ namespace ifcinterface::components::buttons {
QToolButton* makeButton(const QString& text,
const QString& icon_path,
QWidget* parent,
const QSize& minimum_size) {
QWidget* parent) {
auto* button = new QToolButton(parent);
button->setObjectName("ribbonButton");
button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
button->setIcon(components::icons::makeTintedSvgIcon(icon_path));
button->setIconSize(QSize(20, 20));
button->setText(text);
button->setMinimumSize(minimum_size);
button->setMinimumSize(QSize(90, 68));
button->setAutoRaise(false);
return button;
}
+1 -3
View File
@@ -22,7 +22,6 @@
#define IFCINTERFACE_COMPONENTS_BUTTONS_H
#include <QList>
#include <QSize>
class QToolButton;
class QWidget;
@@ -31,8 +30,7 @@ namespace ifcinterface::components::buttons {
QToolButton* makeButton(const QString& text,
const QString& icon_path,
QWidget* parent,
const QSize& minimum_size = QSize(90, 54));
QWidget* parent);
QWidget* makeButtonGroup(const QString& title,
const QList<QToolButton*>& buttons,
+4 -4
View File
@@ -90,7 +90,7 @@ void AddModelDialog::setupUi() {
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(components::style::metrics::padding);
auto* add_ifc = components::buttons::makeButton("Add IFC File", ":/icons/cube.svg", choices, QSize(90, 68));
auto* add_ifc = components::buttons::makeButton("Add IFC File", ":/icons/cube.svg", choices);
connect(add_ifc, &QToolButton::clicked, this, [this]() {
selected_mode_ = SourceMode::IfcFile;
accept();
@@ -100,7 +100,7 @@ void AddModelDialog::setupUi() {
"Add IFC files and load both geometry and data.",
default_description));
auto* add_database = components::buttons::makeButton("Add IFC\nDatabase", ":/icons/database.svg", choices, QSize(90, 68));
auto* add_database = components::buttons::makeButton("Add IFC\nDatabase", ":/icons/database.svg", choices);
connect(add_database, &QToolButton::clicked, this, [this]() {
selected_mode_ = SourceMode::IfcDatabase;
accept();
@@ -110,7 +110,7 @@ void AddModelDialog::setupUi() {
"Add IFC RDB databases for optimised performance",
default_description));
auto* add_geometry = components::buttons::makeButton("Add Geometry", ":/icons/cube-bandage.svg", choices, QSize(90, 68));
auto* add_geometry = components::buttons::makeButton("Add Geometry", ":/icons/cube-bandage.svg", choices);
connect(add_geometry, &QToolButton::clicked, this, [this]() {
selected_mode_ = SourceMode::GeometryOnly;
accept();
@@ -120,7 +120,7 @@ void AddModelDialog::setupUi() {
"Add pure geometry for fast visualisation",
default_description));
auto* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices, QSize(90, 68));
auto* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices);
connect(convert_database, &QToolButton::clicked, this, [description]() {
description->setText("IFC-to-database conversion is coming soon.");
});
+207
View File
@@ -0,0 +1,207 @@
// 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 "Controller.h"
#include "../../ElementRegistry.h"
#include "../../SessionState.h"
#include "../models/Controller.h"
#include "../viewport/Controller.h"
#include "../../../ifcviewer/Federation.h"
#include "../../../ifcviewer/SceneLoader.h"
#include "../../../ifcviewer/ViewportWindow.h"
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
namespace ifcinterface::panels::project {
ProjectController::ProjectController(QWidget* host,
Federation* federation,
ifcinterface::SessionState* session_state,
ifcinterface::ElementRegistry* element_registry,
ViewportWindow* viewport,
ifcinterface::panels::models::ModelsPanelController* models_controller,
ifcinterface::panels::viewport::ViewportController* viewport_controller,
QObject* parent)
: QObject(parent)
, host_(host)
, federation_(federation)
, session_state_(session_state)
, element_registry_(element_registry)
, viewport_(viewport)
, models_controller_(models_controller)
, viewport_controller_(viewport_controller)
{
}
bool ProjectController::newProject() {
SceneLoader* loader = session_state_->loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
host_, "New Project",
"Wait until the current model load finishes before creating a new project.");
return false;
}
if (!confirmDiscardIfDirty()) return false;
clearScene();
federation_->clear();
viewport_controller_->applyFederatedFalseOrigin();
session_state_->setStatusMessage("Project", "Untitled");
session_state_->notifyProjectReset();
return true;
}
bool ProjectController::openProject() {
QFileDialog file_dialog(host_, "Open Project");
file_dialog.setFileMode(QFileDialog::ExistingFile);
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (file_dialog.exec() != QDialog::Accepted) return false;
const QString path = file_dialog.selectedFiles().value(0);
if (path.isEmpty()) return false;
return openProject(path);
}
bool ProjectController::openProject(const QString& path) {
SceneLoader* loader = session_state_->loader();
if (loader && loader->isLoading()) {
QMessageBox::information(
host_, "Open Project",
"Wait until the current model load finishes before opening another project.");
return false;
}
if (!confirmDiscardIfDirty()) return false;
QStringList warnings;
QString err;
if (!federation_->load(path, &warnings, &err)) {
QMessageBox::warning(host_, "Open Project",
QString("Could not open project:\n%1").arg(err));
return false;
}
clearScene();
QStringList paths;
QStringList fed_ids;
for (const auto& model : federation_->models()) {
if (model.source_kind != "local") continue;
if (!QFileInfo::exists(model.source_path)) {
warnings << QString("Source not found, kept in project: %1").arg(model.source_path);
continue;
}
paths << model.source_path;
fed_ids << model.id;
}
models_controller_->loadModels(paths, fed_ids);
if (!warnings.isEmpty()) {
QMessageBox::warning(host_, "Open Project",
"Project opened with warnings:\n\n" + warnings.join("\n"));
}
federation_->markClean();
viewport_controller_->applyFederatedFalseOrigin();
if (federation_->hasHomeView()) {
const auto& hv = federation_->homeView();
viewport_->setCamera(
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
}
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
session_state_->notifyProjectOpened(path);
return true;
}
bool ProjectController::saveProject() {
if (federation_->filePath().isEmpty()) return saveProjectAs();
QString err;
if (!federation_->save(federation_->filePath(), &err)) {
QMessageBox::warning(host_, "Save Project",
QString("Could not save project:\n%1").arg(err));
return false;
}
session_state_->setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName());
session_state_->notifyProjectSaved(federation_->filePath());
return true;
}
bool ProjectController::saveProjectAs() {
QString suggested = federation_->filePath();
if (suggested.isEmpty()) suggested = "project.ifcfed";
QFileDialog file_dialog(host_, "Save Project As", suggested);
file_dialog.setAcceptMode(QFileDialog::AcceptSave);
file_dialog.setFileMode(QFileDialog::AnyFile);
file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)");
file_dialog.setOption(QFileDialog::DontUseNativeDialog, true);
if (file_dialog.exec() != QDialog::Accepted) return false;
QString path = file_dialog.selectedFiles().value(0);
if (path.isEmpty()) return false;
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
return saveProjectAs(path);
}
bool ProjectController::saveProjectAs(const QString& path) {
QString err;
if (!federation_->save(path, &err)) {
QMessageBox::warning(host_, "Save Project",
QString("Could not save project:\n%1").arg(err));
return false;
}
session_state_->setStatusMessage("Project", QFileInfo(path).fileName());
session_state_->notifyProjectSaved(path);
return true;
}
void ProjectController::clearScene() {
viewport_->setSelectedObjectId(0);
session_state_->setSelectedObjectId(0);
session_state_->notifySelectionChanged();
const auto model_ids = session_state_->modelIds();
for (uint32_t mid : model_ids) {
viewport_->removeModel(mid);
session_state_->loader()->removeModel(mid);
}
session_state_->clearModelMappings();
element_registry_->clear();
session_state_->notifyModelsChanged();
}
bool ProjectController::confirmDiscardIfDirty() {
if (!federation_->isDirty()) return true;
const auto result = QMessageBox::question(
host_, "Unsaved Project",
"The current project has unsaved changes. Save before continuing?",
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
QMessageBox::Save);
if (result == QMessageBox::Cancel) return false;
if (result == QMessageBox::Save) return saveProject();
return true;
}
} // namespace ifcinterface::panels::project
+71
View File
@@ -0,0 +1,71 @@
// 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_PANELS_PROJECT_CONTROLLER_H
#define IFCINTERFACE_PANELS_PROJECT_CONTROLLER_H
#include <QObject>
class QWidget;
class Federation;
class ViewportWindow;
namespace ifcinterface { class ElementRegistry; }
namespace ifcinterface { class SessionState; }
namespace ifcinterface::panels::models { class ModelsPanelController; }
namespace ifcinterface::panels::viewport { class ViewportController; }
namespace ifcinterface::panels::project {
class ProjectController : public QObject {
Q_OBJECT
public:
explicit ProjectController(QWidget* host,
Federation* federation,
ifcinterface::SessionState* session_state,
ifcinterface::ElementRegistry* element_registry,
ViewportWindow* viewport,
ifcinterface::panels::models::ModelsPanelController* models_controller,
ifcinterface::panels::viewport::ViewportController* viewport_controller,
QObject* parent = nullptr);
bool newProject();
bool openProject();
bool saveProject();
bool saveProjectAs();
private:
bool openProject(const QString& path);
bool saveProjectAs(const QString& path);
void clearScene();
bool confirmDiscardIfDirty();
QWidget* host_ = nullptr;
Federation* federation_ = nullptr;
ifcinterface::SessionState* session_state_ = nullptr;
ifcinterface::ElementRegistry* element_registry_ = nullptr;
ViewportWindow* viewport_ = nullptr;
ifcinterface::panels::models::ModelsPanelController* models_controller_ = nullptr;
ifcinterface::panels::viewport::ViewportController* viewport_controller_ = nullptr;
};
} // namespace ifcinterface::panels::project
#endif
@@ -148,6 +148,31 @@ void ViewportController::applyFederatedFalseOrigin() {
composeFederatedFalseOrigin(federation->federatedFalseOrigin(), federation->config()));
}
void ViewportController::setHomeView() {
auto camera = viewport_->cameraState();
Federation::HomeView home_view;
home_view.target = camera.target;
home_view.distance = camera.distance;
home_view.yaw = camera.yaw;
home_view.pitch = camera.pitch;
session_state_->federation()->setHomeView(home_view);
session_state_->setStatusMessage("Camera", "Home view updated");
}
void ViewportController::goHomeView() {
Federation* federation = session_state_->federation();
if (!federation->hasHomeView()) {
session_state_->setStatusMessage("Camera", "No home view set for this project");
return;
}
const auto& home_view = federation->homeView();
viewport_->setCamera(
home_view.target.x(), home_view.target.y(), home_view.target.z(),
home_view.distance, home_view.yaw, home_view.pitch);
session_state_->setStatusMessage("Camera", "Home view restored");
}
void ViewportController::maybeGuessFederatedFalseOrigin(uint32_t mid) {
Federation* federation = session_state_->federation();
SceneLoader* loader = session_state_->loader();
@@ -37,6 +37,8 @@ public:
QObject* parent = nullptr);
void applyFederatedFalseOrigin();
void setHomeView();
void goHomeView();
private:
void applyCoordinateOperation(uint32_t mid);