mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
Interface mockup 19
This commit is contained in:
@@ -60,6 +60,8 @@ set(IFCVIEWER_FULL_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Commands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Commands.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/FederationItemModel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/FederationItemModel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/models/View.cpp
|
||||
@@ -71,8 +73,8 @@ set(IFCVIEWER_FULL_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/Panel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/properties/View.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Controller.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Commands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/project/Commands.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/settings/Dialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/modules/spatial_hierarchy/Types.h
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include "../ifcviewer/AppSettings.h"
|
||||
#include "../ifcviewer/Federation.h"
|
||||
#include "../ifcviewer/SceneLoader.h"
|
||||
#include "../ifcviewer/ViewportWindow.h"
|
||||
#include "SessionState.h"
|
||||
#include "components/Buttons.h"
|
||||
@@ -33,7 +32,7 @@
|
||||
#include "modules/todo/Panel.h"
|
||||
#include "modules/models/View.h"
|
||||
#include "modules/models/Panel.h"
|
||||
#include "modules/project/Controller.h"
|
||||
#include "modules/project/Commands.h"
|
||||
#include "modules/properties/View.h"
|
||||
#include "modules/properties/Panel.h"
|
||||
#include "modules/settings/Dialog.h"
|
||||
@@ -150,11 +149,13 @@ QWidget* MainWindow::buildHomeRibbonPage() {
|
||||
|
||||
auto* new_project = components::buttons::makeButton("New Project", ":/icons/plus-square.svg", this);
|
||||
connect(new_project, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->newProject();
|
||||
modules::project::commands::newProject(
|
||||
*session_state_, *this, *viewport_widget_->viewport());
|
||||
});
|
||||
auto* open_project = components::buttons::makeButton("Open Project", ":/icons/download-square.svg", this);
|
||||
connect(open_project, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->openProject();
|
||||
modules::project::commands::openProject(
|
||||
*session_state_, *this, *viewport_widget_->viewport());
|
||||
});
|
||||
auto* open_cloud = components::buttons::makeButton("Open Cloud", ":/icons/cloud-square.svg", this);
|
||||
connect(open_cloud, &QToolButton::clicked, this, [this]() {
|
||||
@@ -166,11 +167,11 @@ QWidget* MainWindow::buildHomeRibbonPage() {
|
||||
});
|
||||
auto* save_project = components::buttons::makeButton("Save Project", ":/icons/floppy-disk.svg", this);
|
||||
connect(save_project, &QToolButton::clicked, this, [this]() {
|
||||
project_controller_->saveProject();
|
||||
modules::project::commands::saveProject(*session_state_, *this);
|
||||
});
|
||||
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();
|
||||
modules::project::commands::saveProjectAs(*session_state_, *this);
|
||||
});
|
||||
|
||||
auto* add_model = components::buttons::makeButton("Add Model", ":/icons/cube.svg", this);
|
||||
@@ -449,43 +450,33 @@ void MainWindow::setupStatus() {
|
||||
status_mode_label_->setText(mode);
|
||||
status_selection_label_->setText(detail);
|
||||
});
|
||||
connect(session_state_, &ifcviewerfull::SessionState::progressBegan, this, [this](const QString&) {
|
||||
// Start in indeterminate mode (spinning bar). The first concrete
|
||||
// setProgress(...) call below switches it to a determinate 0-100 bar.
|
||||
status_progress_bar_->setRange(0, 0);
|
||||
status_progress_bar_->setVisible(true);
|
||||
});
|
||||
connect(session_state_, &ifcviewerfull::SessionState::progressChanged, this, [this](int percent) {
|
||||
if (status_progress_bar_->maximum() == 0) status_progress_bar_->setRange(0, 100);
|
||||
status_progress_bar_->setValue(percent);
|
||||
});
|
||||
connect(session_state_, &ifcviewerfull::SessionState::progressEnded, this, [this]() {
|
||||
status_progress_bar_->setVisible(false);
|
||||
});
|
||||
session_state_->setStatusMessage("Ready", "No selection");
|
||||
}
|
||||
|
||||
void MainWindow::setupLoader() {
|
||||
session_state_->createLoader(viewport_widget_->viewport());
|
||||
auto* loader = session_state_->loader();
|
||||
viewport_view_ = new modules::viewport::ViewportView(
|
||||
session_state_, viewport_widget_->viewport(), this);
|
||||
project_controller_ = new modules::project::ProjectController(
|
||||
this, session_state_, viewport_widget_->viewport(), this);
|
||||
|
||||
// Progress bar — tightly coupled to loader by nature, fine to subscribe direct.
|
||||
connect(loader, &SceneLoader::loadStarted, this,
|
||||
[this](uint32_t, const QString&) {
|
||||
status_progress_bar_->setValue(0);
|
||||
status_progress_bar_->setVisible(true);
|
||||
});
|
||||
connect(loader, &SceneLoader::progressChanged, this,
|
||||
[this](int percent) { status_progress_bar_->setValue(percent); });
|
||||
|
||||
auto hide_progress = [this]() { status_progress_bar_->setVisible(false); };
|
||||
connect(loader, &SceneLoader::loadedFromSidecar, this,
|
||||
[hide_progress](uint32_t, qint64) { hide_progress(); });
|
||||
connect(loader, &SceneLoader::loadedFromStream, this,
|
||||
[this, hide_progress](uint32_t mid, qint64) {
|
||||
modules::models::commands::writeSidecarForLoadedModel(
|
||||
*session_state_, *viewport_widget_->viewport(), mid);
|
||||
hide_progress();
|
||||
});
|
||||
connect(loader, &SceneLoader::loadCancelled, this,
|
||||
[hide_progress](uint32_t) { hide_progress(); });
|
||||
modules::models::commands::addHandlers(
|
||||
*session_state_, *viewport_widget_->viewport(), *this);
|
||||
|
||||
// Load errors surface through SessionState as a session-level signal; the
|
||||
// status text is already set there, we only show the modal here.
|
||||
// status text + progress are already cleared there, we only show the modal.
|
||||
connect(session_state_, &ifcviewerfull::SessionState::loadError, this,
|
||||
[this](const QString& message) {
|
||||
status_progress_bar_->setVisible(false);
|
||||
QMessageBox::warning(this, "IfcViewer", message);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ namespace ifcviewerfull { class SessionState; }
|
||||
namespace ifcviewerfull::components { class TabBar; }
|
||||
namespace ifcviewerfull::modules::models { class ModelsPanel; }
|
||||
namespace ifcviewerfull::modules::models { class ModelsPanelView; }
|
||||
namespace ifcviewerfull::modules::project { class ProjectController; }
|
||||
namespace ifcviewerfull::modules::spatial_hierarchy { class SpatialHierarchyPanel; }
|
||||
namespace ifcviewerfull::modules::spatial_hierarchy { class SpatialHierarchyPanelView; }
|
||||
namespace ifcviewerfull::modules::properties { class PropertiesPanel; }
|
||||
@@ -84,7 +83,6 @@ private:
|
||||
QDockWidget* clash_panel_ = nullptr;
|
||||
QDockWidget* issues_panel_ = nullptr;
|
||||
ifcviewerfull::modules::models::ModelsPanelView* models_view_ = nullptr;
|
||||
ifcviewerfull::modules::project::ProjectController* project_controller_ = nullptr;
|
||||
ifcviewerfull::modules::spatial_hierarchy::SpatialHierarchyPanelView* spatial_view_ = nullptr;
|
||||
ifcviewerfull::modules::properties::PropertiesPanelView* properties_view_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -44,18 +44,21 @@ void SessionState::createLoader(ViewportWindow* viewport) {
|
||||
: QString::number(ms) + " ms";
|
||||
};
|
||||
|
||||
// Translate low-level loader events into session-level signals + status
|
||||
// text so views don't need to subscribe to the loader directly.
|
||||
// Translate low-level loader events into session-level signals, status
|
||||
// text, and progress so views don't need to subscribe to the loader.
|
||||
connect(loader_, &SceneLoader::loadStarted, this,
|
||||
[this](uint32_t, const QString& display_name) {
|
||||
setStatusMessage("Loading", display_name);
|
||||
beginProgress(display_name);
|
||||
});
|
||||
connect(loader_, &SceneLoader::progressChanged, this, &SessionState::setProgress);
|
||||
connect(loader_, &SceneLoader::loadedFromSidecar, this,
|
||||
[this, format_elapsed](uint32_t mid, qint64 elapsed_ms) {
|
||||
setStatusMessage("Loaded",
|
||||
QString("%1 from cache in %2")
|
||||
.arg(loader_->displayName(mid))
|
||||
.arg(format_elapsed(elapsed_ms)));
|
||||
endProgress();
|
||||
emit modelGeometryReady(mid);
|
||||
});
|
||||
connect(loader_, &SceneLoader::loadedFromStream, this,
|
||||
@@ -64,14 +67,18 @@ void SessionState::createLoader(ViewportWindow* viewport) {
|
||||
QString("%1 streamed in %2")
|
||||
.arg(loader_->displayName(mid))
|
||||
.arg(format_elapsed(elapsed_ms)));
|
||||
endProgress();
|
||||
emit modelGeometryReady(mid);
|
||||
emit modelGeometryStreamed(mid);
|
||||
});
|
||||
connect(loader_, &SceneLoader::loadCancelled, this, [this](uint32_t mid) {
|
||||
setStatusMessage("Cancelled", loader_->displayName(mid));
|
||||
endProgress();
|
||||
});
|
||||
connect(loader_, &SceneLoader::loadError, this,
|
||||
[this](uint32_t, const QString& message) {
|
||||
setStatusMessage("Error", message);
|
||||
endProgress();
|
||||
emit loadError(message);
|
||||
});
|
||||
connect(loader_, &SceneLoader::allLoadsFinished, this, [this]() {
|
||||
@@ -89,6 +96,18 @@ void SessionState::setStatusMessage(const QString& mode, const QString& detail)
|
||||
emit statusMessageChanged(status_mode_, status_detail_);
|
||||
}
|
||||
|
||||
void SessionState::beginProgress(const QString& label) {
|
||||
emit progressBegan(label);
|
||||
}
|
||||
|
||||
void SessionState::setProgress(int percent) {
|
||||
emit progressChanged(percent);
|
||||
}
|
||||
|
||||
void SessionState::endProgress() {
|
||||
emit progressEnded();
|
||||
}
|
||||
|
||||
void SessionState::setModelMapping(const QString& fed_id, uint32_t model_id) {
|
||||
fed_id_to_model_id_[fed_id] = model_id;
|
||||
model_id_to_fed_id_[model_id] = fed_id;
|
||||
|
||||
@@ -53,6 +53,13 @@ public:
|
||||
uint32_t selectedObjectId() const { return selected_object_id_; }
|
||||
void setStatusMessage(const QString& mode, const QString& detail);
|
||||
|
||||
// Generic progress reporting for any long-running operation (load,
|
||||
// convert, export, ...). Subscribers (the status bar) react to the
|
||||
// signals; they do not need to know which operation is running.
|
||||
void beginProgress(const QString& label);
|
||||
void setProgress(int percent);
|
||||
void endProgress();
|
||||
|
||||
void setModelMapping(const QString& fed_id, uint32_t model_id);
|
||||
void removeModelMappingByFedId(const QString& fed_id);
|
||||
void clearModelMappings();
|
||||
@@ -79,16 +86,23 @@ signals:
|
||||
// emit this on save/load/reset — projectSaved/Opened/Reset cover those.
|
||||
void federationChanged();
|
||||
void visibilityChanged();
|
||||
// Fires when a model's geometry has been pushed to the viewport. SessionState
|
||||
// emits this internally in response to SceneLoader signals — callers should
|
||||
// not need to fire it themselves.
|
||||
// Fires when a model's geometry has been pushed to the viewport. Fires
|
||||
// for both sidecar-cache and stream loads; subscribers that just need to
|
||||
// re-derive view state (e.g. ViewportView::refresh) listen to this.
|
||||
void modelGeometryReady(uint32_t model_id);
|
||||
// Fires only after a stream load — i.e. when no sidecar cache existed
|
||||
// yet. The sidecar-write subscriber listens to this so it doesn't
|
||||
// re-persist a cache that was just read from disk.
|
||||
void modelGeometryStreamed(uint32_t model_id);
|
||||
// Fires when SceneLoader reports a load failure. SessionState turns the
|
||||
// raw loader signal into a session-level one so views (e.g. the MessageBox)
|
||||
// can subscribe without touching the loader directly.
|
||||
void loadError(const QString& message);
|
||||
void selectionChanged(uint32_t object_id);
|
||||
void statusMessageChanged(const QString& mode, const QString& detail);
|
||||
void progressBegan(const QString& label);
|
||||
void progressChanged(int percent);
|
||||
void progressEnded();
|
||||
|
||||
private:
|
||||
Federation* federation_ = nullptr;
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
#include <QTreeView>
|
||||
@@ -174,6 +173,11 @@ void removeModel(SessionState& s, ViewportWindow& vp, QWidget& host, const QStri
|
||||
s.setStatusMessage("Models", "Model removed");
|
||||
}
|
||||
|
||||
void addHandlers(SessionState& s, ViewportWindow& vp, QObject& context) {
|
||||
QObject::connect(&s, &SessionState::modelGeometryStreamed, &context,
|
||||
[&s, &vp](uint32_t mid) { writeSidecarForLoadedModel(s, vp, mid); });
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
void loadModels(SessionState& s, const QStringList& paths, const QStringList& fed_ids) {
|
||||
@@ -290,19 +294,9 @@ void convertIfcToDatabase(SessionState& s, QWidget& host) {
|
||||
}
|
||||
}
|
||||
|
||||
auto* progress = new QProgressDialog(&host);
|
||||
progress->setWindowTitle("Convert IFC to Database");
|
||||
progress->setLabelText(QString("Converting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName()));
|
||||
progress->setRange(0, 0);
|
||||
progress->setCancelButton(nullptr);
|
||||
progress->setMinimumDuration(0);
|
||||
progress->setWindowModality(Qt::ApplicationModal);
|
||||
progress->setAutoClose(false);
|
||||
progress->setAutoReset(false);
|
||||
progress->show();
|
||||
|
||||
s.beginProgress(QString("Converting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName()));
|
||||
s.setStatusMessage("Converting",
|
||||
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
||||
|
||||
@@ -338,11 +332,10 @@ void convertIfcToDatabase(SessionState& s, QWidget& host) {
|
||||
});
|
||||
|
||||
QObject::connect(thread, &QThread::finished, &host,
|
||||
[&s, host_ptr = &host, thread, progress, timer, error_message, input_path, output_path]() {
|
||||
[&s, host_ptr = &host, thread, timer, error_message, input_path, output_path]() {
|
||||
const qint64 elapsed = timer->elapsed();
|
||||
|
||||
progress->close();
|
||||
progress->deleteLater();
|
||||
s.endProgress();
|
||||
thread->deleteLater();
|
||||
|
||||
if (!error_message->isEmpty()) {
|
||||
@@ -393,19 +386,9 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) {
|
||||
output_path += ".rdbview";
|
||||
}
|
||||
|
||||
auto* progress = new QProgressDialog(&host);
|
||||
progress->setWindowTitle("Export Geometry Database");
|
||||
progress->setLabelText(QString("Exporting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName()));
|
||||
progress->setRange(0, 0);
|
||||
progress->setCancelButton(nullptr);
|
||||
progress->setMinimumDuration(0);
|
||||
progress->setWindowModality(Qt::ApplicationModal);
|
||||
progress->setAutoClose(false);
|
||||
progress->setAutoReset(false);
|
||||
progress->show();
|
||||
|
||||
s.beginProgress(QString("Exporting %1 to %2…")
|
||||
.arg(QFileInfo(input_path).fileName(),
|
||||
QFileInfo(output_path).fileName()));
|
||||
s.setStatusMessage("Exporting",
|
||||
QString("%1 → %2").arg(QFileInfo(input_path).fileName(), QFileInfo(output_path).fileName()));
|
||||
|
||||
@@ -517,11 +500,10 @@ void exportGeometryDatabase(SessionState& s, QWidget& host) {
|
||||
});
|
||||
|
||||
QObject::connect(thread, &QThread::finished, &host,
|
||||
[&s, host_ptr = &host, thread, progress, timer, error_message, input_path, output_path]() {
|
||||
[&s, host_ptr = &host, thread, timer, error_message, input_path, output_path]() {
|
||||
const qint64 elapsed = timer->elapsed();
|
||||
|
||||
progress->close();
|
||||
progress->deleteLater();
|
||||
s.endProgress();
|
||||
thread->deleteLater();
|
||||
|
||||
if (!error_message->isEmpty()) {
|
||||
|
||||
@@ -52,6 +52,12 @@ void openSettings(SessionState& s, QWidget& host);
|
||||
// SceneLoader::loadedFromStream so subsequent loads can skip the stream phase.
|
||||
void writeSidecarForLoadedModel(SessionState& s, ViewportWindow& vp, uint32_t mid);
|
||||
|
||||
// Persistent post-load handler. Call once at app startup. Whenever the
|
||||
// session reports a model was streamed (not loaded from cache), persists a
|
||||
// sidecar so the next load skips the stream phase. Connection lifetime is
|
||||
// tied to `context`.
|
||||
void addHandlers(SessionState& s, ViewportWindow& vp, QObject& context);
|
||||
|
||||
// Internal building blocks shared by commands here and by ProjectController.
|
||||
// These NEVER call notify*() — the caller is responsible for emitting once
|
||||
// at the end of its execution.
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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 "FederationItemModel.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
QStandardItem* siblingVisibilityItem(QStandardItem* name_item) {
|
||||
QStandardItem* parent = name_item->parent();
|
||||
if (!parent) parent = name_item->model()->invisibleRootItem();
|
||||
return parent->child(name_item->row(), 1);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void walkSubtree(QStandardItem* root, F visit) {
|
||||
visit(root);
|
||||
for (int i = 0; i < root->rowCount(); ++i) {
|
||||
walkSubtree(root->child(i, 0), visit);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FederationItemModel::FederationItemModel(Federation* federation, QObject* parent)
|
||||
: QStandardItemModel(parent)
|
||||
, federation_(federation)
|
||||
{
|
||||
setColumnCount(2);
|
||||
rebuildAll();
|
||||
|
||||
connect(federation_, &Federation::groupAdded, this, &FederationItemModel::onGroupAdded);
|
||||
connect(federation_, &Federation::groupRemoved, this, &FederationItemModel::onGroupRemoved);
|
||||
connect(federation_, &Federation::groupChanged, this, &FederationItemModel::onGroupChanged);
|
||||
connect(federation_, &Federation::groupVisibilityChanged, this, &FederationItemModel::onGroupVisibilityChanged);
|
||||
connect(federation_, &Federation::modelAdded, this, &FederationItemModel::onModelAdded);
|
||||
connect(federation_, &Federation::modelRemoved, this, &FederationItemModel::onModelRemoved);
|
||||
connect(federation_, &Federation::modelVisibilityChanged, this, &FederationItemModel::onModelVisibilityChanged);
|
||||
connect(federation_, &Federation::modelGroupChanged, this, &FederationItemModel::onModelGroupChanged);
|
||||
}
|
||||
|
||||
void FederationItemModel::rebuildAll() {
|
||||
clear();
|
||||
setColumnCount(2);
|
||||
id_to_name_item_.clear();
|
||||
|
||||
for (const auto& root_group : federation_->rootGroups()) {
|
||||
appendGroupSubtreeTo(invisibleRootItem(), root_group->id);
|
||||
}
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (!model.group_id.isEmpty()) continue;
|
||||
appendModelTo(invisibleRootItem(), model.id);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeGroupNameItem(const QString& group_id, const QString& display_name) const {
|
||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(":/icons/folder.svg"), display_name);
|
||||
item->setData(group_id, IdRole);
|
||||
item->setData(int(ItemKind::Group), KindRole);
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeModelNameItem(const QString& fed_id, const QString& display_name) const {
|
||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(":/icons/cube.svg"), display_name);
|
||||
item->setData(fed_id, IdRole);
|
||||
item->setData(int(ItemKind::Model), KindRole);
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::makeVisibilityItem(ItemKind kind, bool visible) const {
|
||||
QString icon_path;
|
||||
if (kind == ItemKind::Group) {
|
||||
icon_path = visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg";
|
||||
} else {
|
||||
icon_path = visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg";
|
||||
}
|
||||
auto* item = new QStandardItem(components::icons::makeSvgIcon(icon_path), QString());
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
void FederationItemModel::styleRowVisibility(QStandardItem* name_item, bool visible) const {
|
||||
QStandardItem* vis_item = siblingVisibilityItem(name_item);
|
||||
if (visible) {
|
||||
name_item->setData(QVariant(), Qt::ForegroundRole);
|
||||
if (vis_item) vis_item->setData(QVariant(), Qt::ForegroundRole);
|
||||
} else {
|
||||
const QBrush disabled(QColor(ifcviewerfull::ViewerSettings::instance().color("disabled_text")));
|
||||
name_item->setForeground(disabled);
|
||||
if (vis_item) vis_item->setForeground(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::findItem(const QString& id) const {
|
||||
return id_to_name_item_.value(id, nullptr);
|
||||
}
|
||||
|
||||
QStandardItem* FederationItemModel::parentItemForGroup(const QString& parent_group_id) const {
|
||||
if (parent_group_id.isEmpty()) return invisibleRootItem();
|
||||
QStandardItem* found = findItem(parent_group_id);
|
||||
return found ? found : invisibleRootItem();
|
||||
}
|
||||
|
||||
void FederationItemModel::appendModelTo(QStandardItem* parent_item, const QString& fed_id) {
|
||||
const Federation::Model* model = federation_->findById(fed_id);
|
||||
if (!model) return;
|
||||
auto* name_item = makeModelNameItem(fed_id, model->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Model, federation_->isModelEffectivelyVisible(fed_id));
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
id_to_name_item_.insert(fed_id, name_item);
|
||||
styleRowVisibility(name_item, federation_->isModelEffectivelyVisible(fed_id));
|
||||
}
|
||||
|
||||
void FederationItemModel::appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id) {
|
||||
const Federation::Group* group = federation_->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
auto* name_item = makeGroupNameItem(group_id, group->display_name);
|
||||
auto* vis_item = makeVisibilityItem(ItemKind::Group, group->visible);
|
||||
parent_item->appendRow({name_item, vis_item});
|
||||
id_to_name_item_.insert(group_id, name_item);
|
||||
styleRowVisibility(name_item, group->visible);
|
||||
|
||||
for (const auto& child : group->children) {
|
||||
appendGroupSubtreeTo(name_item, child->id);
|
||||
}
|
||||
for (const auto& model : federation_->models()) {
|
||||
if (model.group_id != group_id) continue;
|
||||
appendModelTo(name_item, model.id);
|
||||
}
|
||||
}
|
||||
|
||||
void FederationItemModel::refreshSubtreeVisibility(QStandardItem* root) {
|
||||
walkSubtree(root, [this](QStandardItem* item) {
|
||||
const QString id = item->data(IdRole).toString();
|
||||
if (id.isEmpty()) return;
|
||||
const auto kind = static_cast<ItemKind>(item->data(KindRole).toInt());
|
||||
bool visible = true;
|
||||
if (kind == ItemKind::Group) {
|
||||
const Federation::Group* g = federation_->findGroupById(id);
|
||||
visible = g && g->visible;
|
||||
} else {
|
||||
visible = federation_->isModelEffectivelyVisible(id);
|
||||
}
|
||||
QStandardItem* vis_item = siblingVisibilityItem(item);
|
||||
if (vis_item) {
|
||||
const QString icon_path = (kind == ItemKind::Group)
|
||||
? (visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg")
|
||||
: (visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg");
|
||||
vis_item->setIcon(components::icons::makeSvgIcon(icon_path));
|
||||
}
|
||||
styleRowVisibility(item, visible);
|
||||
});
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupAdded(const QString& group_id) {
|
||||
const Federation::Group* group = federation_->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
QStandardItem* parent_item = parentItemForGroup(group->parent ? group->parent->id : QString());
|
||||
appendGroupSubtreeTo(parent_item, group_id);
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupRemoved(const QString& group_id) {
|
||||
QStandardItem* item = findItem(group_id);
|
||||
if (!item) return;
|
||||
walkSubtree(item, [this](QStandardItem* descendant) {
|
||||
const QString id = descendant->data(IdRole).toString();
|
||||
if (!id.isEmpty()) id_to_name_item_.remove(id);
|
||||
});
|
||||
QStandardItem* parent_item = item->parent();
|
||||
if (!parent_item) parent_item = invisibleRootItem();
|
||||
parent_item->removeRow(item->row());
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupChanged(const QString& group_id) {
|
||||
QStandardItem* item = findItem(group_id);
|
||||
if (!item) return;
|
||||
const Federation::Group* group = federation_->findGroupById(group_id);
|
||||
if (!group) return;
|
||||
|
||||
QStandardItem* current_parent = item->parent();
|
||||
if (!current_parent) current_parent = invisibleRootItem();
|
||||
QStandardItem* target_parent = parentItemForGroup(group->parent ? group->parent->id : QString());
|
||||
|
||||
if (current_parent == target_parent) {
|
||||
item->setText(group->display_name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reparent: take row from current parent, append at target. Pointers
|
||||
// survive — id_to_name_item_ entries remain valid.
|
||||
QList<QStandardItem*> taken = current_parent->takeRow(item->row());
|
||||
taken.first()->setText(group->display_name);
|
||||
target_parent->appendRow(taken);
|
||||
refreshSubtreeVisibility(taken.first());
|
||||
}
|
||||
|
||||
void FederationItemModel::onGroupVisibilityChanged(const QString& group_id, bool /*visible*/) {
|
||||
QStandardItem* item = findItem(group_id);
|
||||
if (!item) return;
|
||||
refreshSubtreeVisibility(item);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelAdded(const QString& fed_id) {
|
||||
const Federation::Model* model = federation_->findById(fed_id);
|
||||
if (!model) return;
|
||||
QStandardItem* parent_item = parentItemForGroup(model->group_id);
|
||||
appendModelTo(parent_item, fed_id);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelRemoved(const QString& fed_id) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
id_to_name_item_.remove(fed_id);
|
||||
QStandardItem* parent_item = item->parent();
|
||||
if (!parent_item) parent_item = invisibleRootItem();
|
||||
parent_item->removeRow(item->row());
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelVisibilityChanged(const QString& fed_id, bool /*visible*/) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
refreshSubtreeVisibility(item);
|
||||
}
|
||||
|
||||
void FederationItemModel::onModelGroupChanged(const QString& fed_id, const QString& new_group_id) {
|
||||
QStandardItem* item = findItem(fed_id);
|
||||
if (!item) return;
|
||||
QStandardItem* current_parent = item->parent();
|
||||
if (!current_parent) current_parent = invisibleRootItem();
|
||||
QStandardItem* target_parent = parentItemForGroup(new_group_id);
|
||||
if (current_parent == target_parent) return;
|
||||
|
||||
QList<QStandardItem*> taken = current_parent->takeRow(item->row());
|
||||
target_parent->appendRow(taken);
|
||||
refreshSubtreeVisibility(taken.first());
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::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/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_MODULES_MODELS_FEDERATIONITEMMODEL_H
|
||||
#define IFCINTERFACE_MODULES_MODELS_FEDERATIONITEMMODEL_H
|
||||
|
||||
#include "Types.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class Federation;
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
|
||||
// QStandardItemModel that mirrors the Federation tree (groups + models in
|
||||
// two columns: name + visibility icon). Subscribes directly to Federation's
|
||||
// granular signals so each mutation only touches the affected rows — view
|
||||
// state (expansion, selection, scroll) is preserved automatically.
|
||||
//
|
||||
// Coarse session events (project open/reset, theme change) are not the
|
||||
// model's concern: the owning View calls rebuildAll() in those cases.
|
||||
class FederationItemModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Role {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
KindRole = Qt::UserRole + 2,
|
||||
};
|
||||
|
||||
explicit FederationItemModel(Federation* federation, QObject* parent = nullptr);
|
||||
|
||||
// Discard everything and rebuild from current Federation state. Loses
|
||||
// expansion/selection — caller is the only one that knows whether that's
|
||||
// acceptable (e.g. project reset, where there's no prior state worth
|
||||
// preserving anyway).
|
||||
void rebuildAll();
|
||||
|
||||
private slots:
|
||||
void onGroupAdded(const QString& group_id);
|
||||
void onGroupRemoved(const QString& group_id);
|
||||
void onGroupChanged(const QString& group_id);
|
||||
void onGroupVisibilityChanged(const QString& group_id, bool visible);
|
||||
void onModelAdded(const QString& fed_id);
|
||||
void onModelRemoved(const QString& fed_id);
|
||||
void onModelVisibilityChanged(const QString& fed_id, bool visible);
|
||||
void onModelGroupChanged(const QString& fed_id, const QString& new_group_id);
|
||||
|
||||
private:
|
||||
QStandardItem* makeGroupNameItem(const QString& group_id, const QString& display_name) const;
|
||||
QStandardItem* makeModelNameItem(const QString& fed_id, const QString& display_name) const;
|
||||
QStandardItem* makeVisibilityItem(ItemKind kind, bool visible) const;
|
||||
void styleRowVisibility(QStandardItem* name_item, bool visible) const;
|
||||
|
||||
QStandardItem* findItem(const QString& id) const;
|
||||
QStandardItem* parentItemForGroup(const QString& parent_group_id) const;
|
||||
|
||||
void appendModelTo(QStandardItem* parent_item, const QString& fed_id);
|
||||
void appendGroupSubtreeTo(QStandardItem* parent_item, const QString& group_id);
|
||||
void refreshSubtreeVisibility(QStandardItem* root);
|
||||
|
||||
Federation* federation_ = nullptr;
|
||||
QHash<QString, QStandardItem*> id_to_name_item_; // both group_ids and fed_ids
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
#endif
|
||||
@@ -21,25 +21,24 @@
|
||||
#include "Panel.h"
|
||||
|
||||
#include "Commands.h"
|
||||
#include "FederationItemModel.h"
|
||||
#include "View.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
#include "../../SessionState.h"
|
||||
#include "../../components/Section.h"
|
||||
#include "../../components/SvgIcon.h"
|
||||
#include "../../../ifcviewer/Federation.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QDataStream>
|
||||
#include <QDrag>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDragMoveEvent>
|
||||
#include <QDropEvent>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QDropEvent>
|
||||
#include <QSizePolicy>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
#include <functional>
|
||||
#include <QTreeView>
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
|
||||
@@ -47,38 +46,75 @@ namespace {
|
||||
|
||||
constexpr auto kDragMimeType = "application/x-ifcviewerfull-model-items";
|
||||
|
||||
class ModelsTreeWidget : public QTreeWidget {
|
||||
public:
|
||||
explicit ModelsTreeWidget(QWidget* parent = nullptr) : QTreeWidget(parent) {}
|
||||
QString idOf(const QModelIndex& index) {
|
||||
return index.sibling(index.row(), 0).data(FederationItemModel::IdRole).toString();
|
||||
}
|
||||
|
||||
std::function<void(const QStringList&, const QString&)> on_model_drop;
|
||||
std::function<void(const QString&, const QString&)> on_group_drop;
|
||||
ItemKind kindOf(const QModelIndex& index) {
|
||||
return static_cast<ItemKind>(
|
||||
index.sibling(index.row(), 0).data(FederationItemModel::KindRole).toInt());
|
||||
}
|
||||
|
||||
QStringList selectedModelIdsAt(QTreeView* tree, const QModelIndex& clicked_index) {
|
||||
QStringList ids;
|
||||
const QModelIndexList selection = tree->selectionModel()->selectedRows(0);
|
||||
bool clicked_in_selection = false;
|
||||
for (const QModelIndex& index : selection) {
|
||||
if (index == clicked_index.sibling(clicked_index.row(), 0)) {
|
||||
clicked_in_selection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clicked_in_selection) {
|
||||
for (const QModelIndex& index : selection) {
|
||||
if (kindOf(index) == ItemKind::Model) {
|
||||
ids << idOf(index);
|
||||
}
|
||||
}
|
||||
ids.removeDuplicates();
|
||||
} else {
|
||||
ids << idOf(clicked_index);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
constexpr int kVisibilityColumnWidth = 28;
|
||||
|
||||
// QTreeView subclass that handles drag-and-drop. Drop logic dispatches
|
||||
// through commands (not directly into the model) so notifications + status
|
||||
// messages happen the same way as menu-driven moves.
|
||||
class ModelsTreeView : public QTreeView {
|
||||
public:
|
||||
explicit ModelsTreeView(ifcviewerfull::SessionState* session_state, QWidget* parent)
|
||||
: QTreeView(parent), session_state_(session_state) {}
|
||||
|
||||
protected:
|
||||
QStringList mimeTypes() const override {
|
||||
return {QString::fromUtf8(kDragMimeType)};
|
||||
void resizeEvent(QResizeEvent* event) override {
|
||||
QTreeView::resizeEvent(event);
|
||||
if (model() && model()->columnCount() >= 2) {
|
||||
const int vw = viewport()->width();
|
||||
setColumnWidth(0, std::max(40, vw - kVisibilityColumnWidth));
|
||||
setColumnWidth(1, kVisibilityColumnWidth);
|
||||
}
|
||||
}
|
||||
|
||||
QMimeData* mimeData(const QList<QTreeWidgetItem*>& items) const override {
|
||||
Q_UNUSED(items);
|
||||
const QList<QTreeWidgetItem*> selected = selectedItems();
|
||||
if (selected.isEmpty()) return nullptr;
|
||||
void startDrag(Qt::DropActions actions) override {
|
||||
const QModelIndexList selection = selectionModel()->selectedRows(0);
|
||||
if (selection.isEmpty()) return;
|
||||
|
||||
const int first_kind = selected.first()->data(0, Qt::UserRole).toInt();
|
||||
if (first_kind == static_cast<int>(ItemKind::Group) && selected.size() != 1) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto first_kind = kindOf(selection.first());
|
||||
if (first_kind == ItemKind::Group && selection.size() != 1) return;
|
||||
|
||||
QByteArray payload;
|
||||
QDataStream stream(&payload, QIODevice::WriteOnly);
|
||||
stream << first_kind;
|
||||
if (first_kind == static_cast<int>(ItemKind::Group)) {
|
||||
stream << selected.first()->data(0, Qt::UserRole + 1).toString();
|
||||
stream << static_cast<int>(first_kind);
|
||||
if (first_kind == ItemKind::Group) {
|
||||
stream << idOf(selection.first());
|
||||
} else {
|
||||
QStringList ids;
|
||||
for (QTreeWidgetItem* item : selected) {
|
||||
if (item->data(0, Qt::UserRole).toInt() != first_kind) return nullptr;
|
||||
ids.push_back(item->data(0, Qt::UserRole + 1).toString());
|
||||
for (const QModelIndex& index : selection) {
|
||||
if (kindOf(index) != first_kind) return;
|
||||
ids.push_back(idOf(index));
|
||||
}
|
||||
ids.removeDuplicates();
|
||||
stream << ids;
|
||||
@@ -86,11 +122,9 @@ protected:
|
||||
|
||||
auto* mime = new QMimeData();
|
||||
mime->setData(QString::fromUtf8(kDragMimeType), payload);
|
||||
return mime;
|
||||
}
|
||||
|
||||
Qt::DropActions supportedDropActions() const override {
|
||||
return Qt::MoveAction;
|
||||
auto* drag = new QDrag(this);
|
||||
drag->setMimeData(mime);
|
||||
drag->exec(actions);
|
||||
}
|
||||
|
||||
void dragEnterEvent(QDragEnterEvent* event) override {
|
||||
@@ -98,129 +132,100 @@ protected:
|
||||
event->acceptProposedAction();
|
||||
return;
|
||||
}
|
||||
QTreeWidget::dragEnterEvent(event);
|
||||
QTreeView::dragEnterEvent(event);
|
||||
}
|
||||
|
||||
void dragMoveEvent(QDragMoveEvent* event) override {
|
||||
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
QTreeWidget::dragMoveEvent(event);
|
||||
QTreeView::dragMoveEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QString group_id;
|
||||
if (!decodeDropTarget(event->position().toPoint(), group_id)) {
|
||||
QString target_group_id;
|
||||
if (!decodeTargetGroup(event->position().toPoint(), target_group_id) ||
|
||||
!canAcceptDrop(event->mimeData(), indexAt(event->position().toPoint()), target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAcceptDrop(event->mimeData(), itemAt(event->position().toPoint()), group_id)) {
|
||||
event->acceptProposedAction();
|
||||
} else {
|
||||
event->ignore();
|
||||
}
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
void dropEvent(QDropEvent* event) override {
|
||||
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
|
||||
QTreeWidget::dropEvent(event);
|
||||
QTreeView::dropEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QString target_group_id;
|
||||
if (!decodeDropTarget(event->position().toPoint(), target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QTreeWidgetItem* target_item = itemAt(event->position().toPoint());
|
||||
if (!canAcceptDrop(event->mimeData(), target_item, target_group_id)) {
|
||||
if (!decodeTargetGroup(event->position().toPoint(), target_group_id) ||
|
||||
!canAcceptDrop(event->mimeData(), indexAt(event->position().toPoint()), target_group_id)) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray payload = event->mimeData()->data(QString::fromUtf8(kDragMimeType));
|
||||
QDataStream stream(&payload, QIODevice::ReadOnly);
|
||||
int kind = 0;
|
||||
stream >> kind;
|
||||
if (kind == static_cast<int>(ItemKind::Group)) {
|
||||
int kind_int = 0;
|
||||
stream >> kind_int;
|
||||
const auto kind = static_cast<ItemKind>(kind_int);
|
||||
if (kind == ItemKind::Group) {
|
||||
QString group_id;
|
||||
stream >> group_id;
|
||||
if (on_group_drop) on_group_drop(group_id, target_group_id);
|
||||
} else if (kind == static_cast<int>(ItemKind::Model)) {
|
||||
commands::moveGroup(*session_state_, group_id, target_group_id);
|
||||
} else {
|
||||
QStringList ids;
|
||||
stream >> ids;
|
||||
ids.removeDuplicates();
|
||||
if (!ids.isEmpty() && on_model_drop) on_model_drop(ids, target_group_id);
|
||||
} else {
|
||||
event->ignore();
|
||||
return;
|
||||
if (!ids.isEmpty()) commands::moveModels(*session_state_, ids, target_group_id);
|
||||
}
|
||||
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
|
||||
private:
|
||||
bool decodeDropTarget(const QPoint& pos, QString& target_group_id) const {
|
||||
target_group_id.clear();
|
||||
QTreeWidgetItem* target_item = itemAt(pos);
|
||||
if (!target_item) return true;
|
||||
|
||||
const int kind = target_item->data(0, Qt::UserRole).toInt();
|
||||
if (kind != static_cast<int>(ItemKind::Group)) return false;
|
||||
|
||||
target_group_id = target_item->data(0, Qt::UserRole + 1).toString();
|
||||
bool decodeTargetGroup(const QPoint& pos, QString& out) const {
|
||||
out.clear();
|
||||
const QModelIndex index = indexAt(pos);
|
||||
if (!index.isValid()) return true;
|
||||
if (kindOf(index) != ItemKind::Group) return false;
|
||||
out = idOf(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool canAcceptDrop(const QMimeData* mime,
|
||||
QTreeWidgetItem* target_item,
|
||||
const QModelIndex& target_index,
|
||||
const QString& target_group_id) const {
|
||||
QByteArray payload = mime->data(QString::fromUtf8(kDragMimeType));
|
||||
QDataStream stream(&payload, QIODevice::ReadOnly);
|
||||
int kind = 0;
|
||||
stream >> kind;
|
||||
int kind_int = 0;
|
||||
stream >> kind_int;
|
||||
const auto kind = static_cast<ItemKind>(kind_int);
|
||||
|
||||
if (kind == static_cast<int>(ItemKind::Group)) {
|
||||
if (kind == ItemKind::Group) {
|
||||
QString group_id;
|
||||
stream >> group_id;
|
||||
if (group_id.isEmpty()) return false;
|
||||
if (!target_item) return true;
|
||||
if (target_item->data(0, Qt::UserRole).toInt() != static_cast<int>(ItemKind::Group)) return false;
|
||||
if (!target_index.isValid()) return true;
|
||||
if (kindOf(target_index) != ItemKind::Group) return false;
|
||||
if (group_id == target_group_id) return false;
|
||||
for (QTreeWidgetItem* cur = target_item; cur != nullptr; cur = cur->parent()) {
|
||||
if (cur->data(0, Qt::UserRole + 1).toString() == group_id) return false;
|
||||
for (QModelIndex cur = target_index; cur.isValid(); cur = cur.parent()) {
|
||||
if (idOf(cur) == group_id) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (kind == static_cast<int>(ItemKind::Model)) {
|
||||
if (kind == ItemKind::Model) {
|
||||
QStringList ids;
|
||||
stream >> ids;
|
||||
ids.removeDuplicates();
|
||||
if (ids.isEmpty()) return false;
|
||||
return target_item == nullptr || !target_group_id.isNull();
|
||||
return !target_index.isValid() || !target_group_id.isNull();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ifcviewerfull::SessionState* session_state_;
|
||||
};
|
||||
|
||||
QString itemId(QTreeWidgetItem* item) {
|
||||
return item ? item->data(0, Qt::UserRole + 1).toString() : QString();
|
||||
}
|
||||
|
||||
ItemKind itemKind(QTreeWidgetItem* item) {
|
||||
return static_cast<ItemKind>(item->data(0, Qt::UserRole).toInt());
|
||||
}
|
||||
|
||||
QList<QTreeWidgetItem*> selectedItemsOfKind(QTreeWidget* tree, ItemKind kind) {
|
||||
QList<QTreeWidgetItem*> matches;
|
||||
for (QTreeWidgetItem* item : tree->selectedItems()) {
|
||||
if (itemKind(item) == kind) matches.push_back(item);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
@@ -232,11 +237,9 @@ ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
{
|
||||
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||
section->setBodyExpanding(true);
|
||||
auto* tree = new ModelsTreeWidget(section);
|
||||
tree_ = tree;
|
||||
|
||||
tree_ = new ModelsTreeView(session_state_, section);
|
||||
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
tree_->setColumnCount(2);
|
||||
tree_->setHeaderLabels({"Model", ""});
|
||||
tree_->setIconSize(QSize(16, 16));
|
||||
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
@@ -246,29 +249,23 @@ ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
tree_->setDragDropMode(QAbstractItemView::DragDrop);
|
||||
tree_->setDefaultDropAction(Qt::MoveAction);
|
||||
tree_->setUniformRowHeights(true);
|
||||
tree_->header()->setStretchLastSection(false);
|
||||
tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
tree_->header()->setSectionResizeMode(1, QHeaderView::Fixed);
|
||||
tree_->header()->resizeSection(1, 28);
|
||||
tree_->setExpandsOnDoubleClick(false);
|
||||
tree_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
tree_->header()->hide();
|
||||
tree->on_model_drop = [this](const QStringList& ids, const QString& target_group_id) {
|
||||
commands::moveModels(*session_state_, ids, target_group_id);
|
||||
};
|
||||
tree->on_group_drop = [this](const QString& id, const QString& target_group_id) {
|
||||
commands::moveGroup(*session_state_, id, target_group_id);
|
||||
};
|
||||
|
||||
section->addBodyWidget(tree_);
|
||||
addBodyWidget(section);
|
||||
|
||||
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
|
||||
if (!item || column != 1) return;
|
||||
commands::toggleVisibility(*session_state_, itemKind(item), itemId(item));
|
||||
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||
if (!index.isValid() || index.column() != 1) return;
|
||||
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
|
||||
});
|
||||
|
||||
connect(tree_, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
auto* item = tree_->itemAt(pos);
|
||||
connect(tree_, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
const QModelIndex index = tree_->indexAt(pos);
|
||||
QMenu menu(tree_);
|
||||
if (!item) {
|
||||
|
||||
if (!index.isValid()) {
|
||||
QAction* add_group_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this]() {
|
||||
@@ -278,36 +275,34 @@ ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
return;
|
||||
}
|
||||
|
||||
const auto kind = itemKind(item);
|
||||
const QString id = itemId(item);
|
||||
const auto kind = kindOf(index);
|
||||
const QString id = idOf(index);
|
||||
|
||||
QAction* toggle_visibility_action = menu.addAction(
|
||||
QAction* toggle_action = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility");
|
||||
connect(toggle_visibility_action, &QAction::triggered, this, [this, kind, id]() {
|
||||
connect(toggle_action, &QAction::triggered, this, [this, kind, id]() {
|
||||
commands::toggleVisibility(*session_state_, kind, id);
|
||||
});
|
||||
|
||||
if (kind == ItemKind::Group) {
|
||||
QAction* add_group_action = menu.addAction(
|
||||
QAction* add_subgroup = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Subgroup");
|
||||
connect(add_group_action, &QAction::triggered, this, [this, id]() {
|
||||
connect(add_subgroup, &QAction::triggered, this, [this, id]() {
|
||||
commands::addGroup(*session_state_, *this, id);
|
||||
});
|
||||
QAction* rename_group_action = menu.addAction(
|
||||
QAction* rename = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder.svg"), "Rename Group");
|
||||
connect(rename_group_action, &QAction::triggered, this, [this, id]() {
|
||||
connect(rename, &QAction::triggered, this, [this, id]() {
|
||||
commands::renameGroup(*session_state_, *this, id);
|
||||
});
|
||||
|
||||
QMenu* move_menu = menu.addMenu("Move to Parent");
|
||||
QAction* move_root_action = move_menu->addAction("(Root)");
|
||||
connect(move_root_action, &QAction::triggered, this, [this, id]() {
|
||||
QAction* move_root = move_menu->addAction("(Root)");
|
||||
connect(move_root, &QAction::triggered, this, [this, id]() {
|
||||
commands::moveGroup(*session_state_, id, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
const QList<GroupOption> targets =
|
||||
group_list_provider_ ? group_list_provider_(id) : QList<GroupOption>{};
|
||||
for (const auto& target : targets) {
|
||||
for (const auto& target : validMoveTargets(*session_state_->federation(), id)) {
|
||||
QAction* action = move_menu->addAction(target.display_name);
|
||||
const QString target_id = target.id;
|
||||
connect(action, &QAction::triggered, this, [this, id, target_id]() {
|
||||
@@ -315,45 +310,33 @@ ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
});
|
||||
}
|
||||
|
||||
QAction* remove_group_action = menu.addAction(
|
||||
QAction* remove = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group");
|
||||
connect(remove_group_action, &QAction::triggered, this, [this, id]() {
|
||||
connect(remove, &QAction::triggered, this, [this, id]() {
|
||||
commands::removeGroup(*session_state_, *this, id);
|
||||
});
|
||||
} else {
|
||||
QString parent_group_id;
|
||||
if (auto* parent_item = item->parent()) {
|
||||
if (itemKind(parent_item) == ItemKind::Group) {
|
||||
parent_group_id = itemId(parent_item);
|
||||
}
|
||||
const QModelIndex parent_index = index.parent();
|
||||
if (parent_index.isValid() && kindOf(parent_index) == ItemKind::Group) {
|
||||
parent_group_id = idOf(parent_index);
|
||||
}
|
||||
|
||||
QAction* add_group_action = menu.addAction(
|
||||
QAction* add_group = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Group");
|
||||
connect(add_group_action, &QAction::triggered, this, [this, parent_group_id]() {
|
||||
connect(add_group, &QAction::triggered, this, [this, parent_group_id]() {
|
||||
commands::addGroup(*session_state_, *this, parent_group_id);
|
||||
});
|
||||
|
||||
QStringList selected_model_ids;
|
||||
const QList<QTreeWidgetItem*> selected_models = selectedItemsOfKind(tree_, ItemKind::Model);
|
||||
if (selected_models.contains(item)) {
|
||||
for (QTreeWidgetItem* selected : selected_models) {
|
||||
selected_model_ids.push_back(itemId(selected));
|
||||
}
|
||||
selected_model_ids.removeDuplicates();
|
||||
} else {
|
||||
selected_model_ids = {id};
|
||||
}
|
||||
const QStringList selected_model_ids = selectedModelIdsAt(tree_, index);
|
||||
|
||||
QMenu* move_menu = menu.addMenu("Move to Group");
|
||||
QAction* move_root_action = move_menu->addAction("(Root)");
|
||||
connect(move_root_action, &QAction::triggered, this, [this, selected_model_ids]() {
|
||||
QAction* move_root = move_menu->addAction("(Root)");
|
||||
connect(move_root, &QAction::triggered, this, [this, selected_model_ids]() {
|
||||
commands::moveModels(*session_state_, selected_model_ids, QString());
|
||||
});
|
||||
move_menu->addSeparator();
|
||||
const QList<GroupOption> targets =
|
||||
group_list_provider_ ? group_list_provider_(QString()) : QList<GroupOption>{};
|
||||
for (const auto& target : targets) {
|
||||
for (const auto& target : validMoveTargets(*session_state_->federation(), QString())) {
|
||||
QAction* action = move_menu->addAction(target.display_name);
|
||||
const QString target_id = target.id;
|
||||
connect(action, &QAction::triggered, this, [this, selected_model_ids, target_id]() {
|
||||
@@ -361,9 +344,9 @@ ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
});
|
||||
}
|
||||
|
||||
QAction* remove_model_action = menu.addAction(
|
||||
QAction* remove = menu.addAction(
|
||||
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
|
||||
connect(remove_model_action, &QAction::triggered, this, [this, id]() {
|
||||
connect(remove, &QAction::triggered, this, [this, id]() {
|
||||
commands::removeModel(*session_state_, *viewport_, *this, id);
|
||||
});
|
||||
}
|
||||
@@ -375,46 +358,17 @@ ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
});
|
||||
}
|
||||
|
||||
void ModelsPanel::setNodes(const QList<TreeNode>& nodes) {
|
||||
tree_->clear();
|
||||
const bool has_hierarchy = std::any_of(nodes.begin(), nodes.end(), [](const TreeNode& node) {
|
||||
return !node.children.isEmpty() || node.kind == ItemKind::Group;
|
||||
});
|
||||
tree_->setRootIsDecorated(has_hierarchy);
|
||||
for (const auto& node : nodes) {
|
||||
addNode(tree_->invisibleRootItem(), node);
|
||||
}
|
||||
void ModelsPanel::setModel(FederationItemModel* model) {
|
||||
model_ = model;
|
||||
tree_->setModel(model);
|
||||
// Columns are sized by ModelsTreeView::resizeEvent — header is hidden so
|
||||
// there's no user-facing resize affordance, and Stretch mode on
|
||||
// non-last sections proved unreliable here. Manual sizing is simpler.
|
||||
tree_->header()->setMinimumSectionSize(16);
|
||||
tree_->header()->setStretchLastSection(false);
|
||||
tree_->setColumnWidth(0, std::max(40, tree_->viewport()->width() - kVisibilityColumnWidth));
|
||||
tree_->setColumnWidth(1, kVisibilityColumnWidth);
|
||||
tree_->expandAll();
|
||||
}
|
||||
|
||||
void ModelsPanel::setGroupListProvider(GroupListProvider provider) {
|
||||
group_list_provider_ = std::move(provider);
|
||||
}
|
||||
|
||||
void ModelsPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
|
||||
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
|
||||
item->setData(0, Qt::UserRole, static_cast<int>(node.kind));
|
||||
item->setData(0, Qt::UserRole + 1, node.id);
|
||||
item->setSizeHint(0, QSize(0, 24));
|
||||
|
||||
if (node.kind == ItemKind::Group) {
|
||||
item->setIcon(0, components::icons::makeSvgIcon(":/icons/folder.svg"));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
|
||||
} else {
|
||||
item->setIcon(0, components::icons::makeSvgIcon(":/icons/cube.svg"));
|
||||
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye-solid.svg" : ":/icons/eye-closed.svg"));
|
||||
}
|
||||
|
||||
if (!node.visible) {
|
||||
const QBrush disabled_brush(
|
||||
QColor(ifcviewerfull::ViewerSettings::instance().color("disabled_text")));
|
||||
item->setForeground(0, disabled_brush);
|
||||
item->setForeground(1, disabled_brush);
|
||||
}
|
||||
|
||||
for (const auto& child : node.children) {
|
||||
addNode(item, child);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
@@ -25,42 +25,34 @@
|
||||
|
||||
#include "../../components/Panel.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QTreeView;
|
||||
class ViewportWindow;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
|
||||
// The widget for the Models dock. Owns no domain state; its click handlers
|
||||
// call commands directly. The View tells it what to render (setNodes) and
|
||||
// supplies derived data for the right-click menu (setGroupListProvider).
|
||||
class FederationItemModel;
|
||||
|
||||
// The widget for the Models dock. Owns no domain state; click handlers call
|
||||
// commands directly. The QTreeView reads from a FederationItemModel which
|
||||
// subscribes to Federation's granular signals — view state (expansion,
|
||||
// selection, scroll) is preserved across mutations automatically.
|
||||
class ModelsPanel : public components::Panel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Returns the groups a move operation may target. If exclude_subtree_root
|
||||
// is non-empty, that group + its descendants are excluded so a group can't
|
||||
// be moved into its own subtree. For model moves the panel passes an empty
|
||||
// string and gets every group back.
|
||||
using GroupListProvider =
|
||||
std::function<QList<GroupOption>(const QString& exclude_subtree_root)>;
|
||||
|
||||
explicit ModelsPanel(ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
void setNodes(const QList<TreeNode>& nodes);
|
||||
void setGroupListProvider(GroupListProvider provider);
|
||||
// Owned externally (the View constructs and owns the model). The panel
|
||||
// assigns it to the tree view; same model can outlive setModel calls.
|
||||
void setModel(FederationItemModel* model);
|
||||
|
||||
private:
|
||||
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
|
||||
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
QTreeWidget* tree_ = nullptr;
|
||||
GroupListProvider group_list_provider_;
|
||||
QTreeView* tree_ = nullptr;
|
||||
FederationItemModel* model_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "View.h"
|
||||
|
||||
#include "FederationItemModel.h"
|
||||
#include "Panel.h"
|
||||
|
||||
#include "../../ViewerSettings.h"
|
||||
@@ -30,32 +31,6 @@ namespace ifcviewerfull::modules::models {
|
||||
|
||||
namespace {
|
||||
|
||||
TreeNode makeGroupNode(const Federation* federation, const Federation::Group* group) {
|
||||
TreeNode node;
|
||||
node.id = group->id;
|
||||
node.name = group->display_name;
|
||||
node.kind = ItemKind::Group;
|
||||
node.visible = group->visible;
|
||||
|
||||
for (const auto& child_group : group->children) {
|
||||
node.children.append(makeGroupNode(federation, child_group.get()));
|
||||
}
|
||||
|
||||
for (const auto& model : federation->models()) {
|
||||
if (model.group_id != group->id) continue;
|
||||
node.children.append({
|
||||
model.id,
|
||||
model.display_name,
|
||||
ItemKind::Model,
|
||||
federation->isModelEffectivelyVisible(model.id),
|
||||
{}
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Walks the federation's group tree, appending every group except those under
|
||||
// exclude_subtree_root (used to prevent a group from being moved into itself).
|
||||
void collectGroupsRecursive(const Federation::Group* group,
|
||||
const QString& exclude_subtree_root,
|
||||
QList<GroupOption>& out) {
|
||||
@@ -68,54 +43,33 @@ void collectGroupsRecursive(const Federation::Group* group,
|
||||
|
||||
} // namespace
|
||||
|
||||
QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
const QString& exclude_subtree_root) {
|
||||
QList<GroupOption> out;
|
||||
for (const auto& root : federation.rootGroups()) {
|
||||
collectGroupsRecursive(root.get(), exclude_subtree_root, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ModelsPanelView::ModelsPanelView(ModelsPanel* widget,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, widget_(widget)
|
||||
, session_state_(session_state)
|
||||
, model_(new FederationItemModel(session_state->federation(), this))
|
||||
{
|
||||
connect(session_state_, &SessionState::modelsChanged, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::federationChanged, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::visibilityChanged, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::projectReset, this, &ModelsPanelView::refresh);
|
||||
connect(session_state_, &SessionState::projectOpened, this, [this](const QString&) { refresh(); });
|
||||
widget_->setModel(model_);
|
||||
|
||||
// Coarse signals: full rebuild + re-style. The granular Federation
|
||||
// signals are handled inside FederationItemModel and don't reach here.
|
||||
auto rebuild = [this]() { model_->rebuildAll(); };
|
||||
connect(session_state_, &SessionState::projectReset, this, rebuild);
|
||||
connect(session_state_, &SessionState::projectOpened, this, rebuild);
|
||||
connect(&ifcviewerfull::ViewerSettings::instance(),
|
||||
&ifcviewerfull::ViewerSettings::themeChanged,
|
||||
this, &ModelsPanelView::refresh);
|
||||
|
||||
widget_->setGroupListProvider([this](const QString& exclude_subtree_root) {
|
||||
return groupListForMove(exclude_subtree_root);
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ModelsPanelView::refresh() {
|
||||
Federation* federation = session_state_->federation();
|
||||
QList<TreeNode> nodes;
|
||||
for (const auto& root_group : federation->rootGroups()) {
|
||||
nodes.append(makeGroupNode(federation, root_group.get()));
|
||||
}
|
||||
for (const auto& model : federation->models()) {
|
||||
if (!model.group_id.isEmpty()) continue;
|
||||
nodes.append({
|
||||
model.id,
|
||||
model.display_name,
|
||||
ItemKind::Model,
|
||||
federation->isModelEffectivelyVisible(model.id),
|
||||
{}
|
||||
});
|
||||
}
|
||||
widget_->setNodes(nodes);
|
||||
}
|
||||
|
||||
QList<GroupOption> ModelsPanelView::groupListForMove(const QString& exclude_subtree_root) const {
|
||||
QList<GroupOption> out;
|
||||
for (const auto& root : session_state_->federation()->rootGroups()) {
|
||||
collectGroupsRecursive(root.get(), exclude_subtree_root, out);
|
||||
}
|
||||
return out;
|
||||
this, rebuild);
|
||||
}
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
@@ -25,15 +25,26 @@
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Federation;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
|
||||
namespace ifcviewerfull::modules::models {
|
||||
|
||||
class FederationItemModel;
|
||||
class ModelsPanel;
|
||||
|
||||
// Subscribes to SessionState and re-derives panel state (tree nodes, valid
|
||||
// move targets) from the federation. The panel calls commands directly for
|
||||
// input, so this object is purely state→view; it has no command knowledge.
|
||||
// Pure derivation used by ModelsPanel when building its "move to..." menus.
|
||||
// Walks the federation's group tree and returns every group except those in
|
||||
// the subtree rooted at exclude_subtree_root (skip a group's own subtree to
|
||||
// prevent a cyclic move). Pass an empty exclude_subtree_root to get every
|
||||
// group back.
|
||||
QList<GroupOption> validMoveTargets(const Federation& federation,
|
||||
const QString& exclude_subtree_root);
|
||||
|
||||
// Owns the FederationItemModel, hands it to the panel, and listens to the
|
||||
// coarse session signals (project open/reset, theme change) — those are the
|
||||
// "rebuild from scratch" cases the model itself doesn't subscribe to.
|
||||
// Granular Federation events are handled inside the model.
|
||||
class ModelsPanelView : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -42,11 +53,9 @@ public:
|
||||
QObject* parent = nullptr);
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
QList<GroupOption> groupListForMove(const QString& exclude_subtree_root) const;
|
||||
|
||||
ModelsPanel* widget_ = nullptr;
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
FederationItemModel* model_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::models
|
||||
|
||||
+90
-103
@@ -18,7 +18,7 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "Controller.h"
|
||||
#include "Commands.h"
|
||||
|
||||
#include "../../ElementRegistry.h"
|
||||
#include "../../SessionState.h"
|
||||
@@ -31,71 +31,61 @@
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace ifcviewerfull::modules::project {
|
||||
namespace ifcviewerfull::modules::project::commands {
|
||||
|
||||
ProjectController::ProjectController(QWidget* host,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, host_(host)
|
||||
, session_state_(session_state)
|
||||
, viewport_(viewport)
|
||||
{
|
||||
namespace {
|
||||
|
||||
// Pure helper — clears the loaded scene without emitting any signals. The
|
||||
// caller (newProject / openProject) emits projectReset / projectOpened once
|
||||
// the whole flow finishes.
|
||||
void clearScene(SessionState& s, ViewportWindow& vp) {
|
||||
vp.setSelectedObjectId(0);
|
||||
s.setSelectedObjectId(0);
|
||||
for (uint32_t mid : s.modelIds()) {
|
||||
vp.removeModel(mid);
|
||||
s.loader()->removeModel(mid);
|
||||
}
|
||||
s.clearModelMappings();
|
||||
s.elementRegistry()->clear();
|
||||
}
|
||||
|
||||
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();
|
||||
session_state_->federation()->clear();
|
||||
session_state_->setStatusMessage("Project", "Untitled");
|
||||
session_state_->notifyProjectReset();
|
||||
// Returns false if the user cancelled (i.e. don't proceed with the destructive
|
||||
// op). Handles the Save → Discard → Cancel branch including a follow-on save.
|
||||
bool confirmDiscardIfDirty(SessionState& s, QWidget& host) {
|
||||
if (!s.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(s, host);
|
||||
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();
|
||||
bool openProjectAt(SessionState& s, QWidget& host, ViewportWindow& vp, const QString& path) {
|
||||
SceneLoader* loader = s.loader();
|
||||
if (loader && loader->isLoading()) {
|
||||
QMessageBox::information(
|
||||
host_, "Open Project",
|
||||
&host, "Open Project",
|
||||
"Wait until the current model load finishes before opening another project.");
|
||||
return false;
|
||||
}
|
||||
if (!confirmDiscardIfDirty()) return false;
|
||||
if (!confirmDiscardIfDirty(s, host)) return false;
|
||||
|
||||
QStringList warnings;
|
||||
QString err;
|
||||
if (!session_state_->federation()->load(path, &warnings, &err)) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
if (!s.federation()->load(path, &warnings, &err)) {
|
||||
QMessageBox::warning(&host, "Open Project",
|
||||
QString("Could not open project:\n%1").arg(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
clearScene();
|
||||
clearScene(s, vp);
|
||||
|
||||
QStringList paths;
|
||||
QStringList fed_ids;
|
||||
for (const auto& model : session_state_->federation()->models()) {
|
||||
for (const auto& model : s.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);
|
||||
@@ -104,43 +94,77 @@ bool ProjectController::openProject(const QString& path) {
|
||||
paths << model.source_path;
|
||||
fed_ids << model.id;
|
||||
}
|
||||
ifcviewerfull::modules::models::commands::detail::loadModels(*session_state_, paths, fed_ids);
|
||||
modules::models::commands::detail::loadModels(s, paths, fed_ids);
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
QMessageBox::warning(host_, "Open Project",
|
||||
QMessageBox::warning(&host, "Open Project",
|
||||
"Project opened with warnings:\n\n" + warnings.join("\n"));
|
||||
}
|
||||
|
||||
session_state_->federation()->markClean();
|
||||
if (session_state_->federation()->hasHomeView()) {
|
||||
const auto& hv = session_state_->federation()->homeView();
|
||||
viewport_->setCamera(
|
||||
hv.target.x(), hv.target.y(), hv.target.z(), hv.distance, hv.yaw, hv.pitch);
|
||||
s.federation()->markClean();
|
||||
if (s.federation()->hasHomeView()) {
|
||||
const auto& hv = s.federation()->homeView();
|
||||
vp.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);
|
||||
s.setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
s.notifyProjectOpened(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProject() {
|
||||
if (session_state_->federation()->filePath().isEmpty()) return saveProjectAs();
|
||||
|
||||
bool saveProjectTo(SessionState& s, QWidget& host, const QString& path) {
|
||||
QString err;
|
||||
if (!session_state_->federation()->save(session_state_->federation()->filePath(), &err)) {
|
||||
QMessageBox::warning(host_, "Save Project",
|
||||
if (!s.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(session_state_->federation()->filePath()).fileName());
|
||||
session_state_->notifyProjectSaved(session_state_->federation()->filePath());
|
||||
s.setStatusMessage("Project", QFileInfo(path).fileName());
|
||||
s.notifyProjectSaved(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs() {
|
||||
QString suggested = session_state_->federation()->filePath();
|
||||
} // namespace
|
||||
|
||||
bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
|
||||
SceneLoader* loader = s.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(s, host)) return false;
|
||||
|
||||
clearScene(s, vp);
|
||||
s.federation()->clear();
|
||||
s.setStatusMessage("Project", "Untitled");
|
||||
s.notifyProjectReset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp) {
|
||||
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 openProjectAt(s, host, vp, path);
|
||||
}
|
||||
|
||||
bool saveProject(SessionState& s, QWidget& host) {
|
||||
if (s.federation()->filePath().isEmpty()) return saveProjectAs(s, host);
|
||||
return saveProjectTo(s, host, s.federation()->filePath());
|
||||
}
|
||||
|
||||
bool saveProjectAs(SessionState& s, QWidget& host) {
|
||||
QString suggested = s.federation()->filePath();
|
||||
if (suggested.isEmpty()) suggested = "project.ifcfed";
|
||||
|
||||
QFileDialog file_dialog(host_, "Save Project As", suggested);
|
||||
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 (*)");
|
||||
@@ -150,44 +174,7 @@ bool ProjectController::saveProjectAs() {
|
||||
QString path = file_dialog.selectedFiles().value(0);
|
||||
if (path.isEmpty()) return false;
|
||||
if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed";
|
||||
return saveProjectAs(path);
|
||||
return saveProjectTo(s, host, path);
|
||||
}
|
||||
|
||||
bool ProjectController::saveProjectAs(const QString& path) {
|
||||
QString err;
|
||||
if (!session_state_->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() {
|
||||
// Helper for newProject / openProject. Does not emit any notifies; the
|
||||
// caller emits projectReset / projectOpened once at the end of its flow.
|
||||
viewport_->setSelectedObjectId(0);
|
||||
session_state_->setSelectedObjectId(0);
|
||||
for (uint32_t mid : session_state_->modelIds()) {
|
||||
viewport_->removeModel(mid);
|
||||
session_state_->loader()->removeModel(mid);
|
||||
}
|
||||
session_state_->clearModelMappings();
|
||||
session_state_->elementRegistry()->clear();
|
||||
}
|
||||
|
||||
bool ProjectController::confirmDiscardIfDirty() {
|
||||
if (!session_state_->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 ifcviewerfull::modules::project
|
||||
} // namespace ifcviewerfull::modules::project::commands
|
||||
+12
-29
@@ -18,42 +18,25 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef IFCINTERFACE_PANELS_PROJECT_CONTROLLER_H
|
||||
#define IFCINTERFACE_PANELS_PROJECT_CONTROLLER_H
|
||||
#ifndef IFCINTERFACE_MODULES_PROJECT_COMMANDS_H
|
||||
#define IFCINTERFACE_MODULES_PROJECT_COMMANDS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QWidget;
|
||||
class ViewportWindow;
|
||||
namespace ifcviewerfull { class SessionState; }
|
||||
|
||||
namespace ifcviewerfull::modules::project {
|
||||
namespace ifcviewerfull::modules::project::commands {
|
||||
|
||||
class ProjectController : public QObject {
|
||||
Q_OBJECT
|
||||
// User-facing commands. Each owns its own dialogs and confirmations; each
|
||||
// emits exactly one notify() at the end (projectReset / projectOpened /
|
||||
// projectSaved) so views refresh once per command.
|
||||
bool newProject(SessionState& s, QWidget& host, ViewportWindow& vp);
|
||||
bool openProject(SessionState& s, QWidget& host, ViewportWindow& vp);
|
||||
bool saveProject(SessionState& s, QWidget& host);
|
||||
bool saveProjectAs(SessionState& s, QWidget& host);
|
||||
|
||||
public:
|
||||
explicit ProjectController(QWidget* host,
|
||||
ifcviewerfull::SessionState* session_state,
|
||||
ViewportWindow* viewport,
|
||||
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;
|
||||
ifcviewerfull::SessionState* session_state_ = nullptr;
|
||||
ViewportWindow* viewport_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ifcviewerfull::modules::project
|
||||
} // namespace ifcviewerfull::modules::project::commands
|
||||
|
||||
#endif
|
||||
@@ -483,8 +483,10 @@ QString Federation::addModel(const QString& source_path,
|
||||
m.source_kind = "local";
|
||||
m.source_path = QDir::cleanPath(QFileInfo(source_path).absoluteFilePath());
|
||||
models_.push_back(std::move(m));
|
||||
const QString new_id = models_.back().id;
|
||||
setDirty(true);
|
||||
return models_.back().id;
|
||||
emit modelAdded(new_id);
|
||||
return new_id;
|
||||
}
|
||||
|
||||
void Federation::removeModel(const QString& fed_id) {
|
||||
@@ -492,6 +494,7 @@ void Federation::removeModel(const QString& fed_id) {
|
||||
if (it->id == fed_id) {
|
||||
models_.erase(it);
|
||||
setDirty(true);
|
||||
emit modelRemoved(fed_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ signals:
|
||||
// to dirtyChanged from the corresponding setters.
|
||||
void configChanged();
|
||||
void federatedFalseOriginChanged();
|
||||
void modelAdded(const QString& fed_id);
|
||||
void modelRemoved(const QString& fed_id);
|
||||
void modelTransformationChanged(const QString& fed_id);
|
||||
void modelVisibilityChanged(const QString& fed_id, bool visible);
|
||||
void modelGroupChanged(const QString& fed_id, const QString& group_id);
|
||||
|
||||
Reference in New Issue
Block a user