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