Files
IfcOpenShell/src/ifcviewer-full/modules/models/Panel.cpp
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

375 lines
15 KiB
C++
Raw Normal View History

2026-05-09 22:00:19 +10:00
// 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 "Panel.h"
2026-05-15 17:08:08 +10:00
#include "Commands.h"
2026-05-16 17:04:23 +10:00
#include "FederationItemModel.h"
#include "View.h"
2026-05-15 17:08:08 +10:00
2026-05-16 17:04:23 +10:00
#include "../../SessionState.h"
2026-05-09 22:00:19 +10:00
#include "../../components/Section.h"
#include "../../components/SvgIcon.h"
2026-05-16 17:04:23 +10:00
#include "../../../ifcviewer/Federation.h"
2026-05-09 22:00:19 +10:00
2026-05-14 10:20:43 +10:00
#include <QDataStream>
2026-05-16 17:04:23 +10:00
#include <QDrag>
2026-05-14 10:20:43 +10:00
#include <QDragEnterEvent>
#include <QDragMoveEvent>
2026-05-16 17:04:23 +10:00
#include <QDropEvent>
2026-05-09 22:00:19 +10:00
#include <QHeaderView>
#include <QMenu>
2026-05-14 10:20:43 +10:00
#include <QMimeData>
2026-05-10 22:04:03 +10:00
#include <QSizePolicy>
2026-05-16 17:04:23 +10:00
#include <QTreeView>
2026-05-14 10:20:43 +10:00
2026-05-15 07:22:31 +10:00
namespace ifcviewerfull::modules::models {
2026-05-09 22:00:19 +10:00
2026-05-14 10:20:43 +10:00
namespace {
2026-05-15 07:22:31 +10:00
constexpr auto kDragMimeType = "application/x-ifcviewerfull-model-items";
2026-05-14 10:20:43 +10:00
2026-05-16 17:04:23 +10:00
QString idOf(const QModelIndex& index) {
return index.sibling(index.row(), 0).data(FederationItemModel::IdRole).toString();
}
2026-05-14 10:20:43 +10:00
2026-05-16 17:04:23 +10:00
ItemKind kindOf(const QModelIndex& index) {
return static_cast<ItemKind>(
index.sibling(index.row(), 0).data(FederationItemModel::KindRole).toInt());
}
2026-05-14 10:20:43 +10:00
2026-05-16 17:04:23 +10:00
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);
2026-05-14 10:20:43 +10:00
}
2026-05-16 17:04:23 +10:00
return ids;
}
2026-05-14 10:20:43 +10:00
2026-05-16 17:04:23 +10:00
constexpr int kVisibilityColumnWidth = 28;
2026-05-14 10:20:43 +10:00
2026-05-16 17:04:23 +10:00
// 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:
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);
2026-05-14 10:20:43 +10:00
}
2026-05-16 17:04:23 +10:00
}
void startDrag(Qt::DropActions actions) override {
const QModelIndexList selection = selectionModel()->selectedRows(0);
if (selection.isEmpty()) return;
const auto first_kind = kindOf(selection.first());
if (first_kind == ItemKind::Group && selection.size() != 1) return;
2026-05-14 10:20:43 +10:00
QByteArray payload;
QDataStream stream(&payload, QIODevice::WriteOnly);
2026-05-16 17:04:23 +10:00
stream << static_cast<int>(first_kind);
if (first_kind == ItemKind::Group) {
stream << idOf(selection.first());
2026-05-14 10:20:43 +10:00
} else {
QStringList ids;
2026-05-16 17:04:23 +10:00
for (const QModelIndex& index : selection) {
if (kindOf(index) != first_kind) return;
ids.push_back(idOf(index));
2026-05-14 10:20:43 +10:00
}
ids.removeDuplicates();
stream << ids;
}
auto* mime = new QMimeData();
mime->setData(QString::fromUtf8(kDragMimeType), payload);
2026-05-16 17:04:23 +10:00
auto* drag = new QDrag(this);
drag->setMimeData(mime);
drag->exec(actions);
2026-05-14 10:20:43 +10:00
}
void dragEnterEvent(QDragEnterEvent* event) override {
if (event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
event->acceptProposedAction();
return;
}
2026-05-16 17:04:23 +10:00
QTreeView::dragEnterEvent(event);
2026-05-14 10:20:43 +10:00
}
void dragMoveEvent(QDragMoveEvent* event) override {
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
2026-05-16 17:04:23 +10:00
QTreeView::dragMoveEvent(event);
2026-05-14 10:20:43 +10:00
return;
}
2026-05-16 17:04:23 +10:00
QString target_group_id;
if (!decodeTargetGroup(event->position().toPoint(), target_group_id) ||
!canAcceptDrop(event->mimeData(), indexAt(event->position().toPoint()), target_group_id)) {
2026-05-14 10:20:43 +10:00
event->ignore();
return;
}
2026-05-16 17:04:23 +10:00
event->acceptProposedAction();
2026-05-14 10:20:43 +10:00
}
void dropEvent(QDropEvent* event) override {
if (!event->mimeData()->hasFormat(QString::fromUtf8(kDragMimeType))) {
2026-05-16 17:04:23 +10:00
QTreeView::dropEvent(event);
2026-05-14 10:20:43 +10:00
return;
}
QString target_group_id;
2026-05-16 17:04:23 +10:00
if (!decodeTargetGroup(event->position().toPoint(), target_group_id) ||
!canAcceptDrop(event->mimeData(), indexAt(event->position().toPoint()), target_group_id)) {
2026-05-14 10:20:43 +10:00
event->ignore();
return;
}
QByteArray payload = event->mimeData()->data(QString::fromUtf8(kDragMimeType));
QDataStream stream(&payload, QIODevice::ReadOnly);
2026-05-16 17:04:23 +10:00
int kind_int = 0;
stream >> kind_int;
const auto kind = static_cast<ItemKind>(kind_int);
if (kind == ItemKind::Group) {
2026-05-14 10:20:43 +10:00
QString group_id;
stream >> group_id;
2026-05-16 17:04:23 +10:00
commands::moveGroup(*session_state_, group_id, target_group_id);
} else {
2026-05-14 10:20:43 +10:00
QStringList ids;
stream >> ids;
ids.removeDuplicates();
2026-05-16 17:04:23 +10:00
if (!ids.isEmpty()) commands::moveModels(*session_state_, ids, target_group_id);
2026-05-14 10:20:43 +10:00
}
event->acceptProposedAction();
}
private:
2026-05-16 17:04:23 +10:00
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);
2026-05-14 10:20:43 +10:00
return true;
}
bool canAcceptDrop(const QMimeData* mime,
2026-05-16 17:04:23 +10:00
const QModelIndex& target_index,
2026-05-14 10:20:43 +10:00
const QString& target_group_id) const {
QByteArray payload = mime->data(QString::fromUtf8(kDragMimeType));
QDataStream stream(&payload, QIODevice::ReadOnly);
2026-05-16 17:04:23 +10:00
int kind_int = 0;
stream >> kind_int;
const auto kind = static_cast<ItemKind>(kind_int);
2026-05-14 10:20:43 +10:00
2026-05-16 17:04:23 +10:00
if (kind == ItemKind::Group) {
2026-05-14 10:20:43 +10:00
QString group_id;
stream >> group_id;
if (group_id.isEmpty()) return false;
2026-05-16 17:04:23 +10:00
if (!target_index.isValid()) return true;
if (kindOf(target_index) != ItemKind::Group) return false;
2026-05-14 10:20:43 +10:00
if (group_id == target_group_id) return false;
2026-05-16 17:04:23 +10:00
for (QModelIndex cur = target_index; cur.isValid(); cur = cur.parent()) {
if (idOf(cur) == group_id) return false;
2026-05-14 10:20:43 +10:00
}
return true;
}
2026-05-16 17:04:23 +10:00
if (kind == ItemKind::Model) {
2026-05-14 10:20:43 +10:00
QStringList ids;
stream >> ids;
ids.removeDuplicates();
if (ids.isEmpty()) return false;
2026-05-16 17:04:23 +10:00
return !target_index.isValid() || !target_group_id.isNull();
2026-05-14 10:20:43 +10:00
}
return false;
}
2026-05-16 17:04:23 +10:00
ifcviewerfull::SessionState* session_state_;
};
2026-05-14 10:20:43 +10:00
} // namespace
2026-05-15 17:08:08 +10:00
ModelsPanel::ModelsPanel(ifcviewerfull::SessionState* session_state,
ViewportWindow* viewport,
QWidget* parent)
2026-05-09 22:00:19 +10:00
: components::Panel("Models", nullptr, parent, true)
2026-05-15 17:08:08 +10:00
, session_state_(session_state)
, viewport_(viewport)
2026-05-09 22:00:19 +10:00
{
auto* section = new components::Section("", components::SectionHeaderMode::Hidden, this);
2026-05-10 22:04:03 +10:00
section->setBodyExpanding(true);
2026-05-16 17:04:23 +10:00
tree_ = new ModelsTreeView(session_state_, section);
2026-05-10 22:04:03 +10:00
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
2026-05-09 22:00:19 +10:00
tree_->setIconSize(QSize(16, 16));
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
tree_->setContextMenuPolicy(Qt::CustomContextMenu);
2026-05-14 10:20:43 +10:00
tree_->setDragEnabled(true);
tree_->viewport()->setAcceptDrops(true);
tree_->setDropIndicatorShown(true);
tree_->setDragDropMode(QAbstractItemView::DragDrop);
tree_->setDefaultDropAction(Qt::MoveAction);
2026-05-09 22:00:19 +10:00
tree_->setUniformRowHeights(true);
2026-05-16 17:04:23 +10:00
tree_->setExpandsOnDoubleClick(false);
tree_->setEditTriggers(QAbstractItemView::NoEditTriggers);
2026-05-09 22:00:19 +10:00
tree_->header()->hide();
2026-05-16 17:04:23 +10:00
2026-05-09 22:00:19 +10:00
section->addBodyWidget(tree_);
addBodyWidget(section);
2026-05-16 17:04:23 +10:00
connect(tree_, &QTreeView::clicked, this, [this](const QModelIndex& index) {
if (!index.isValid() || index.column() != 1) return;
commands::toggleVisibility(*session_state_, kindOf(index), idOf(index));
2026-05-09 22:00:19 +10:00
});
2026-05-16 17:04:23 +10:00
connect(tree_, &QTreeView::customContextMenuRequested, this, [this](const QPoint& pos) {
const QModelIndex index = tree_->indexAt(pos);
2026-05-09 22:00:19 +10:00
QMenu menu(tree_);
2026-05-16 17:04:23 +10:00
if (!index.isValid()) {
2026-05-14 10:20:43 +10:00
QAction* add_group_action = menu.addAction(
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "Add Group");
connect(add_group_action, &QAction::triggered, this, [this]() {
2026-05-15 17:08:08 +10:00
commands::addGroup(*session_state_, *this, QString());
2026-05-09 22:00:19 +10:00
});
menu.exec(tree_->viewport()->mapToGlobal(pos));
return;
}
2026-05-16 17:04:23 +10:00
const auto kind = kindOf(index);
const QString id = idOf(index);
2026-05-09 22:00:19 +10:00
2026-05-16 17:04:23 +10:00
QAction* toggle_action = menu.addAction(
2026-05-14 10:20:43 +10:00
components::icons::makeSvgIcon(":/icons/eye.svg"), "Toggle Visibility");
2026-05-16 17:04:23 +10:00
connect(toggle_action, &QAction::triggered, this, [this, kind, id]() {
2026-05-15 17:08:08 +10:00
commands::toggleVisibility(*session_state_, kind, id);
2026-05-09 22:00:19 +10:00
});
if (kind == ItemKind::Group) {
2026-05-16 17:04:23 +10:00
QAction* add_subgroup = menu.addAction(
2026-05-14 10:20:43 +10:00
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Subgroup");
2026-05-16 17:04:23 +10:00
connect(add_subgroup, &QAction::triggered, this, [this, id]() {
2026-05-15 17:08:08 +10:00
commands::addGroup(*session_state_, *this, id);
2026-05-09 22:00:19 +10:00
});
2026-05-16 17:04:23 +10:00
QAction* rename = menu.addAction(
2026-05-14 10:20:43 +10:00
components::icons::makeSvgIcon(":/icons/folder.svg"), "Rename Group");
2026-05-16 17:04:23 +10:00
connect(rename, &QAction::triggered, this, [this, id]() {
2026-05-15 17:08:08 +10:00
commands::renameGroup(*session_state_, *this, id);
2026-05-14 10:20:43 +10:00
});
2026-05-15 17:08:08 +10:00
2026-05-14 10:20:43 +10:00
QMenu* move_menu = menu.addMenu("Move to Parent");
2026-05-16 17:04:23 +10:00
QAction* move_root = move_menu->addAction("(Root)");
connect(move_root, &QAction::triggered, this, [this, id]() {
2026-05-15 17:08:08 +10:00
commands::moveGroup(*session_state_, id, QString());
2026-05-14 10:20:43 +10:00
});
move_menu->addSeparator();
2026-05-16 17:04:23 +10:00
for (const auto& target : validMoveTargets(*session_state_->federation(), id)) {
2026-05-15 17:08:08 +10:00
QAction* action = move_menu->addAction(target.display_name);
const QString target_id = target.id;
connect(action, &QAction::triggered, this, [this, id, target_id]() {
commands::moveGroup(*session_state_, id, target_id);
});
2026-05-14 10:20:43 +10:00
}
2026-05-15 17:08:08 +10:00
2026-05-16 17:04:23 +10:00
QAction* remove = menu.addAction(
2026-05-14 10:20:43 +10:00
components::icons::makeSvgIcon(":/icons/folder-minus.svg"), "Remove Group");
2026-05-16 17:04:23 +10:00
connect(remove, &QAction::triggered, this, [this, id]() {
2026-05-15 17:08:08 +10:00
commands::removeGroup(*session_state_, *this, id);
2026-05-09 22:00:19 +10:00
});
} else {
2026-05-14 10:20:43 +10:00
QString parent_group_id;
2026-05-16 17:04:23 +10:00
const QModelIndex parent_index = index.parent();
if (parent_index.isValid() && kindOf(parent_index) == ItemKind::Group) {
parent_group_id = idOf(parent_index);
2026-05-14 10:20:43 +10:00
}
2026-05-16 17:04:23 +10:00
QAction* add_group = menu.addAction(
2026-05-14 10:20:43 +10:00
components::icons::makeSvgIcon(":/icons/folder-plus.svg"), "New Group");
2026-05-16 17:04:23 +10:00
connect(add_group, &QAction::triggered, this, [this, parent_group_id]() {
2026-05-15 17:08:08 +10:00
commands::addGroup(*session_state_, *this, parent_group_id);
2026-05-14 10:20:43 +10:00
});
2026-05-16 17:04:23 +10:00
const QStringList selected_model_ids = selectedModelIdsAt(tree_, index);
2026-05-14 10:20:43 +10:00
QMenu* move_menu = menu.addMenu("Move to Group");
2026-05-16 17:04:23 +10:00
QAction* move_root = move_menu->addAction("(Root)");
connect(move_root, &QAction::triggered, this, [this, selected_model_ids]() {
2026-05-15 17:08:08 +10:00
commands::moveModels(*session_state_, selected_model_ids, QString());
2026-05-14 10:20:43 +10:00
});
move_menu->addSeparator();
2026-05-16 17:04:23 +10:00
for (const auto& target : validMoveTargets(*session_state_->federation(), QString())) {
2026-05-15 17:08:08 +10:00
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]() {
commands::moveModels(*session_state_, selected_model_ids, target_id);
});
2026-05-14 10:20:43 +10:00
}
2026-05-15 17:08:08 +10:00
2026-05-16 17:04:23 +10:00
QAction* remove = menu.addAction(
2026-05-14 10:20:43 +10:00
components::icons::makeSvgIcon(":/icons/minus-square.svg"), "Remove Model");
2026-05-16 17:04:23 +10:00
connect(remove, &QAction::triggered, this, [this, id]() {
2026-05-15 17:08:08 +10:00
commands::removeModel(*session_state_, *viewport_, *this, id);
2026-05-09 22:00:19 +10:00
});
}
menu.exec(tree_->viewport()->mapToGlobal(pos));
});
2026-05-15 17:08:08 +10:00
connect(this, &components::Panel::settingsRequested, this, [this]() {
commands::openSettings(*session_state_, *this);
});
2026-05-09 22:00:19 +10:00
}
2026-05-16 17:04:23 +10:00
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);
2026-05-09 22:00:19 +10:00
tree_->expandAll();
}
2026-05-15 07:22:31 +10:00
} // namespace ifcviewerfull::modules::models