diff --git a/src/interface/ElementRegistry.cpp b/src/interface/ElementRegistry.cpp index 7583ff3dd6..12f84e5d23 100644 --- a/src/interface/ElementRegistry.cpp +++ b/src/interface/ElementRegistry.cpp @@ -39,6 +39,10 @@ void ElementRegistry::bindLoader(SceneLoader* loader) { this, &ElementRegistry::onStreamedElementsReady); } +void ElementRegistry::clear() { + elements_.clear(); +} + std::optional ElementRegistry::findBasicElementInfo(uint32_t object_id) const { auto it = elements_.find(object_id); if (it == elements_.end()) return std::nullopt; diff --git a/src/interface/ElementRegistry.h b/src/interface/ElementRegistry.h index b059c10c5d..e6ee785184 100644 --- a/src/interface/ElementRegistry.h +++ b/src/interface/ElementRegistry.h @@ -51,6 +51,7 @@ public: explicit ElementRegistry(QObject* parent = nullptr); void bindLoader(SceneLoader* loader); + void clear(); std::optional findBasicElementInfo(uint32_t object_id) const; std::optional findEntity(uint32_t object_id) const; diff --git a/src/interface/MainWindow.cpp b/src/interface/MainWindow.cpp index 45e55b1574..4185c2b016 100644 --- a/src/interface/MainWindow.cpp +++ b/src/interface/MainWindow.cpp @@ -21,12 +21,15 @@ #include "MainWindow.h" #include "../ifcviewer/AppSettings.h" +#include "../ifcviewer/Federation.h" #include "../ifcviewer/SceneLoader.h" #include "../ifcviewer/ViewportWindow.h" #include "ElementRegistry.h" +#include "components/Buttons.h" #include "components/Panel.h" #include "components/Style.h" -#include "components/SvgIcon.h" +#include "components/Tabs.h" +#include "panels/add_model/Dialog.h" #include "panels/todo/Widget.h" #include "panels/models/View.h" #include "panels/models/Widget.h" @@ -38,15 +41,17 @@ #include #include +#include #include #include #include #include +#include #include #include #include #include -#include +#include #include #include @@ -55,7 +60,12 @@ namespace ifcinterface::shell { MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { + federation_ = new Federation(this); element_registry_ = new ifcinterface::ElementRegistry(this); + connect(federation_, &Federation::dirtyChanged, this, [this](bool dirty) { + setWindowModified(dirty); + updateWindowTitle(); + }); setupChrome(); setupViewport(); setupPanels(); @@ -67,46 +77,18 @@ MainWindow::MainWindow(QWidget* parent) void MainWindow::setupChrome() { setObjectName("appWindow"); - setWindowTitle("IfcOpenShell Interface"); setDockOptions(QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks | QMainWindow::GroupedDragging); - setStyleSheet(components::style::buildAppStyleSheet()); + updateWindowTitle(); } QToolButton* MainWindow::makeRibbonAction(const QString& text, const QString& icon_path) { - auto* button = new QToolButton(this); - button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); - button->setIcon(icon_path.endsWith(".svg") - ? components::icons::makeTintedSvgIcon(icon_path) - : QIcon(icon_path)); - button->setIconSize(QSize(20, 20)); - button->setText(text); - button->setMinimumSize(QSize(68, 54)); - button->setObjectName("ribbonButton"); - button->setAutoRaise(false); - return button; + return components::buttons::makeButton(text, icon_path, this, QSize(90, 54)); } QWidget* MainWindow::makeRibbonGroup(const QString& title, const QList& buttons) { - auto* group = new QFrame(this); - group->setObjectName("ribbonGroup"); - auto* group_layout = new QVBoxLayout(group); - group_layout->setContentsMargins(8, 6, 8, 4); - group_layout->setSpacing(4); - auto* button_row = new QHBoxLayout(); - button_row->setContentsMargins(0, 0, 0, 0); - button_row->setSpacing(4); - for (auto* button : buttons) { - button_row->addWidget(button); - } - auto* label = new QLabel(title, group); - label->setObjectName("ribbonGroupLabel"); - label->setProperty("textRole", "secondary"); - label->setAlignment(Qt::AlignCenter); - group_layout->addLayout(button_row); - group_layout->addWidget(label); - return group; + return components::buttons::makeButtonGroup(title, buttons, this); } void MainWindow::setStatusMessage(const QString& mode, const QString& detail) { @@ -137,13 +119,9 @@ QWidget* MainWindow::buildHomeRibbonPage() { row->setSpacing(0); auto* new_project = makeRibbonAction("New Project", ":/icons/plus-square.svg"); - connect(new_project, &QToolButton::clicked, this, [this]() { - setStatusMessage("Project", "New Project coming soon"); - }); + connect(new_project, &QToolButton::clicked, this, &MainWindow::onNewProject); auto* open_project = makeRibbonAction("Open Project", ":/icons/download-square.svg"); - connect(open_project, &QToolButton::clicked, this, [this]() { - setStatusMessage("Project", "Open Project coming soon"); - }); + connect(open_project, &QToolButton::clicked, this, &MainWindow::onOpenProject); auto* open_cloud = makeRibbonAction("Open Cloud", ":/icons/cloud-square.svg"); connect(open_cloud, &QToolButton::clicked, this, [this]() { setStatusMessage("Project", "Open Cloud Project coming soon"); @@ -153,13 +131,9 @@ QWidget* MainWindow::buildHomeRibbonPage() { setStatusMessage("Project", "Open Recent coming soon"); }); auto* save_project = makeRibbonAction("Save Project", ":/icons/floppy-disk.svg"); - connect(save_project, &QToolButton::clicked, this, [this]() { - setStatusMessage("Project", "Save Project coming soon"); - }); + connect(save_project, &QToolButton::clicked, this, &MainWindow::onSaveProject); auto* save_project_as = makeRibbonAction("Save As", ":/icons/floppy-disk-arrow-in.svg"); - connect(save_project_as, &QToolButton::clicked, this, [this]() { - setStatusMessage("Project", "Save Project As coming soon"); - }); + connect(save_project_as, &QToolButton::clicked, this, &MainWindow::onSaveProjectAs); auto* add_model = makeRibbonAction("Add Model", ":/icons/cube.svg"); connect(add_model, &QToolButton::clicked, this, &MainWindow::onAddFiles); @@ -322,14 +296,12 @@ void MainWindow::setupRibbon() { shell_layout->setContentsMargins(0, 0, 0, 0); shell_layout->setSpacing(0); - ribbon_tabs_ = new QTabBar(shell); + ribbon_tabs_ = new components::TabBar(shell); ribbon_tabs_->addTab("Home"); ribbon_tabs_->addTab("Navigate"); ribbon_tabs_->addTab("Inspect"); ribbon_tabs_->addTab("Panels"); ribbon_tabs_->setCurrentIndex(0); - ribbon_tabs_->setExpanding(false); - ribbon_tabs_->setDrawBase(false); auto* ribbon_band = new QFrame(shell); ribbon_band->setObjectName("ribbonBand"); @@ -347,7 +319,7 @@ void MainWindow::setupRibbon() { shell_layout->addWidget(ribbon_tabs_); shell_layout->addWidget(ribbon_band); - connect(ribbon_tabs_, &QTabBar::currentChanged, + connect(ribbon_tabs_, &components::TabBar::currentChanged, ribbon_pages_, &QStackedWidget::setCurrentIndex); setMenuWidget(shell); @@ -481,8 +453,16 @@ void MainWindow::setupLoader() { } void MainWindow::addFiles(const QStringList& paths) { - if (paths.isEmpty()) return; - loader_->addFiles(paths); + QStringList accepted_paths; + QStringList accepted_fed_ids; + for (const auto& path : paths) { + const QString fed_id = federation_->addModel(path); + if (fed_id.isEmpty()) continue; + accepted_paths << path; + accepted_fed_ids << fed_id; + } + loadModelsFromPaths(accepted_paths, accepted_fed_ids); + updateWindowTitle(); } QString MainWindow::formatElapsed(qint64 ms) const { @@ -492,12 +472,225 @@ QString MainWindow::formatElapsed(qint64 ms) const { } void MainWindow::onAddFiles() { - const QStringList paths = QFileDialog::getOpenFileNames( - this, "Add IFC Files", QString(), - "IFC Viewer Cache (*.ifcview)"); + panels::add_model::AddModelDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) return; + + QStringList paths; + switch (dialog.selectedMode()) { + case panels::add_model::SourceMode::IfcFile: { + QFileDialog file_dialog(this, "Add IFC Files"); + file_dialog.setFileMode(QFileDialog::ExistingFiles); + file_dialog.setNameFilter("IFC Files (*.ifc);;All Files (*)"); + file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); + if (file_dialog.exec() == QDialog::Accepted) { + paths = file_dialog.selectedFiles(); + } + break; + } + case panels::add_model::SourceMode::IfcDatabase: { + QFileDialog database_dialog(this, "Add IFC Databases"); + database_dialog.setFileMode(QFileDialog::Directory); + database_dialog.setOption(QFileDialog::ShowDirsOnly, true); + database_dialog.setOption(QFileDialog::DontResolveSymlinks, true); + database_dialog.setOption(QFileDialog::DontUseNativeDialog, true); + if (auto* list = database_dialog.findChild("listView")) { + list->setSelectionMode(QAbstractItemView::ExtendedSelection); + } + if (auto* tree = database_dialog.findChild()) { + tree->setSelectionMode(QAbstractItemView::ExtendedSelection); + } + if (database_dialog.exec() == QDialog::Accepted) { + paths = database_dialog.selectedFiles(); + } + break; + } + case panels::add_model::SourceMode::GeometryOnly: { + QFileDialog file_dialog(this, "Add Geometry Only"); + file_dialog.setFileMode(QFileDialog::ExistingFiles); + file_dialog.setNameFilter("IFC Viewer Cache (*.ifcview);;All Files (*)"); + file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); + if (file_dialog.exec() == QDialog::Accepted) { + paths = file_dialog.selectedFiles(); + } + break; + } + case panels::add_model::SourceMode::None: + return; + } addFiles(paths); } +void MainWindow::clearScene() { + if (viewport_) viewport_->setSelectedObjectId(0); + if (properties_view_) properties_view_->clearSelection(); + + const auto model_ids = model_id_to_fed_id_.keys(); + for (uint32_t mid : model_ids) { + viewport_->removeModel(mid); + loader_->removeModel(mid); + } + + fed_id_to_model_id_.clear(); + model_id_to_fed_id_.clear(); + element_registry_->clear(); +} + +bool MainWindow::confirmDiscardIfDirty() { + if (!federation_->isDirty()) return true; + const auto result = QMessageBox::question( + this, "Unsaved Project", + "The current project has unsaved changes. Save before continuing?", + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, + QMessageBox::Save); + if (result == QMessageBox::Cancel) return false; + if (result == QMessageBox::Save) return saveProject(); + return true; +} + +void MainWindow::loadModelsFromPaths(const QStringList& paths, const QStringList& fed_ids) { + if (paths.isEmpty()) return; + const auto ids = loader_->addFiles(paths); + for (int i = 0; i < paths.size() && i < static_cast(ids.size()) && i < fed_ids.size(); ++i) { + fed_id_to_model_id_[fed_ids[i]] = ids[i]; + model_id_to_fed_id_[ids[i]] = fed_ids[i]; + } +} + +bool MainWindow::openProject(const QString& path) { + if (loader_ && loader_->isLoading()) { + QMessageBox::information( + this, "Open Project", + "Wait until the current model load finishes before opening another project."); + return false; + } + if (!confirmDiscardIfDirty()) return false; + + QStringList warnings; + QString err; + if (!federation_->load(path, &warnings, &err)) { + QMessageBox::warning(this, "Open Project", + QString("Could not open project:\n%1").arg(err)); + return false; + } + + clearScene(); + + QStringList paths; + QStringList fed_ids; + QStringList missing; + for (const auto& model : federation_->models()) { + if (model.source_kind != "local") continue; + if (!QFileInfo::exists(model.source_path)) { + missing << model.source_path; + continue; + } + paths << model.source_path; + fed_ids << model.id; + } + loadModelsFromPaths(paths, fed_ids); + + for (const auto& msg : missing) { + warnings << QString("Source not found, kept in project: %1").arg(msg); + } + if (!warnings.isEmpty()) { + QMessageBox::warning(this, "Open Project", + "Project opened with warnings:\n\n" + warnings.join("\n")); + } + + federation_->markClean(); + if (federation_->hasHomeView()) { + const auto& hv = federation_->homeView(); + viewport_->setCamera(hv.target.x(), hv.target.y(), hv.target.z(), + hv.distance, hv.yaw, hv.pitch); + } + updateWindowTitle(); + setStatusMessage("Project", QFileInfo(path).fileName()); + return true; +} + +bool MainWindow::saveProject() { + if (federation_->filePath().isEmpty()) return saveProjectAs(); + + QString err; + if (!federation_->save(federation_->filePath(), &err)) { + QMessageBox::warning(this, "Save Project", + QString("Could not save project:\n%1").arg(err)); + return false; + } + updateWindowTitle(); + setStatusMessage("Project", QFileInfo(federation_->filePath()).fileName()); + return true; +} + +bool MainWindow::saveProjectAs() { + QString suggested = federation_->filePath(); + if (suggested.isEmpty()) suggested = "project.ifcfed"; + + QFileDialog file_dialog(this, "Save Project As", suggested); + file_dialog.setAcceptMode(QFileDialog::AcceptSave); + file_dialog.setFileMode(QFileDialog::AnyFile); + file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)"); + file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); + if (file_dialog.exec() != QDialog::Accepted) return false; + QString path = file_dialog.selectedFiles().value(0); + if (path.isEmpty()) return false; + if (!path.endsWith(".ifcfed", Qt::CaseInsensitive)) path += ".ifcfed"; + + QString err; + if (!federation_->save(path, &err)) { + QMessageBox::warning(this, "Save Project", + QString("Could not save project:\n%1").arg(err)); + return false; + } + updateWindowTitle(); + setStatusMessage("Project", QFileInfo(path).fileName()); + return true; +} + +void MainWindow::updateWindowTitle() { + const QString project_path = federation_ ? federation_->filePath() : QString(); + if (project_path.isEmpty() && (!federation_ || federation_->models().empty())) { + setWindowTitle("IfcOpenShell Interface"); + } else if (project_path.isEmpty()) { + setWindowTitle("untitled[*] - IfcOpenShell Interface"); + } else { + setWindowTitle(QFileInfo(project_path).fileName() + "[*] - IfcOpenShell Interface"); + } +} + +void MainWindow::onNewProject() { + if (loader_ && loader_->isLoading()) { + QMessageBox::information( + this, "New Project", + "Wait until the current model load finishes before creating a new project."); + return; + } + if (!confirmDiscardIfDirty()) return; + clearScene(); + federation_->clear(); + updateWindowTitle(); + setStatusMessage("Project", "Untitled"); +} + +void MainWindow::onOpenProject() { + QFileDialog file_dialog(this, "Open Project"); + file_dialog.setFileMode(QFileDialog::ExistingFile); + file_dialog.setNameFilter("IFC Federation (*.ifcfed);;All Files (*)"); + file_dialog.setOption(QFileDialog::DontUseNativeDialog, true); + if (file_dialog.exec() != QDialog::Accepted) return; + const QString path = file_dialog.selectedFiles().value(0); + if (path.isEmpty()) return; + openProject(path); +} + +void MainWindow::onSaveProject() { + saveProject(); +} + +void MainWindow::onSaveProjectAs() { + saveProjectAs(); +} + void MainWindow::onLoadStarted(uint32_t /*mid*/, QString display_name) { status_mode_label_->setText("Loading"); status_selection_label_->setText(display_name); diff --git a/src/interface/MainWindow.h b/src/interface/MainWindow.h index 0ca4fc317a..a50f3da91c 100644 --- a/src/interface/MainWindow.h +++ b/src/interface/MainWindow.h @@ -21,17 +21,19 @@ #ifndef IFCINTERFACE_SHELL_MAINWINDOW_H #define IFCINTERFACE_SHELL_MAINWINDOW_H +#include #include #include class QLabel; class QDockWidget; class QStackedWidget; -class QTabBar; class QToolButton; +class Federation; class ViewportWindow; class SceneLoader; namespace ifcinterface { class ElementRegistry; } +namespace ifcinterface::components { class TabBar; } namespace ifcinterface::panels::models { class ModelsPanelView; } namespace ifcinterface::panels::spatial_hierarchy { class SpatialHierarchyPanelView; } namespace ifcinterface::panels::properties { class PropertiesPanelView; } @@ -54,6 +56,13 @@ private: QWidget* buildNavigateRibbonPage(); QWidget* buildInspectRibbonPage(); QWidget* buildPanelsRibbonPage(); + void clearScene(); + bool confirmDiscardIfDirty(); + void loadModelsFromPaths(const QStringList& paths, const QStringList& fed_ids); + bool openProject(const QString& path); + bool saveProject(); + bool saveProjectAs(); + void updateWindowTitle(); QToolButton* makeRibbonAction(const QString& text, const QString& icon_path); QWidget* makeRibbonGroup(const QString& title, const QList& buttons); QToolButton* makePanelToggle(const QString& text, QDockWidget* dock); @@ -69,12 +78,17 @@ private slots: void onLoadCancelled(uint32_t mid); void onLoadError(uint32_t mid, QString message); void onAllLoadsFinished(); + void onNewProject(); + void onOpenProject(); + void onSaveProject(); + void onSaveProjectAs(); private: + Federation* federation_ = nullptr; QLabel* status_mode_label_ = nullptr; QLabel* status_selection_label_ = nullptr; QLabel* status_perf_label_ = nullptr; - QTabBar* ribbon_tabs_ = nullptr; + ifcinterface::components::TabBar* ribbon_tabs_ = nullptr; QStackedWidget* ribbon_pages_ = nullptr; ViewportWindow* viewport_ = nullptr; SceneLoader* loader_ = nullptr; @@ -93,6 +107,8 @@ private: ifcinterface::panels::models::ModelsPanelView* models_view_ = nullptr; ifcinterface::panels::spatial_hierarchy::SpatialHierarchyPanelView* spatial_view_ = nullptr; ifcinterface::panels::properties::PropertiesPanelView* properties_view_ = nullptr; + QHash fed_id_to_model_id_; + QHash model_id_to_fed_id_; }; } // namespace ifcinterface::shell diff --git a/src/interface/components/Buttons.cpp b/src/interface/components/Buttons.cpp new file mode 100644 index 0000000000..219717bb16 --- /dev/null +++ b/src/interface/components/Buttons.cpp @@ -0,0 +1,78 @@ +// 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 "Buttons.h" + +#include "SvgIcon.h" + +#include +#include +#include +#include +#include + +namespace ifcinterface::components::buttons { + +QToolButton* makeButton(const QString& text, + const QString& icon_path, + QWidget* parent, + const QSize& minimum_size) { + auto* button = new QToolButton(parent); + button->setObjectName("ribbonButton"); + button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + button->setIcon(components::icons::makeTintedSvgIcon(icon_path)); + button->setIconSize(QSize(20, 20)); + button->setText(text); + button->setMinimumSize(minimum_size); + button->setAutoRaise(false); + return button; +} + +QWidget* makeButtonGroup(const QString& title, + const QList& buttons, + QWidget* parent, + bool trailing_separator, + int vertical_spacing) { + auto* group = new QFrame(parent); + group->setObjectName("ribbonGroup"); + group->setProperty("separator", trailing_separator); + + auto* group_layout = new QVBoxLayout(group); + group_layout->setContentsMargins(8, 6, 8, 4); + group_layout->setSpacing(vertical_spacing); + + auto* button_row = new QHBoxLayout(); + button_row->setContentsMargins(0, 0, 0, 0); + button_row->setSpacing(4); + for (auto* button : buttons) { + button_row->addWidget(button); + } + + auto* label = new QLabel(title, group); + label->setObjectName("ribbonGroupLabel"); + label->setProperty("textRole", "secondary"); + label->setAlignment(Qt::AlignCenter); + + group_layout->addLayout(button_row); + group_layout->addWidget(label); + return group; +} + +} // namespace ifcinterface::components::buttons diff --git a/src/interface/components/Buttons.h b/src/interface/components/Buttons.h new file mode 100644 index 0000000000..9dbc0eb724 --- /dev/null +++ b/src/interface/components/Buttons.h @@ -0,0 +1,45 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_COMPONENTS_BUTTONS_H +#define IFCINTERFACE_COMPONENTS_BUTTONS_H + +#include +#include + +class QToolButton; +class QWidget; + +namespace ifcinterface::components::buttons { + +QToolButton* makeButton(const QString& text, + const QString& icon_path, + QWidget* parent, + const QSize& minimum_size = QSize(90, 54)); + +QWidget* makeButtonGroup(const QString& title, + const QList& buttons, + QWidget* parent, + bool trailing_separator = true, + int vertical_spacing = 4); + +} // namespace ifcinterface::components::buttons + +#endif diff --git a/src/interface/components/Dialog.cpp b/src/interface/components/Dialog.cpp new file mode 100644 index 0000000000..6b111fcc88 --- /dev/null +++ b/src/interface/components/Dialog.cpp @@ -0,0 +1,79 @@ +// 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 "Dialog.h" + +#include "Style.h" + +#include +#include +#include +#include + +namespace ifcinterface::components { + +Dialog::Dialog(QWidget* parent, bool scrollable) + : QDialog(parent) +{ + auto* outer_layout = new QVBoxLayout(this); + outer_layout->setContentsMargins(style::metrics::padding, + style::metrics::padding, + style::metrics::padding, + style::metrics::padding); + outer_layout->setSpacing(0); + + auto* frame = new QFrame(this); + frame->setObjectName("panel"); + auto* frame_layout = new QVBoxLayout(frame); + frame_layout->setContentsMargins(0, + style::metrics::section_body_padding, + 0, + style::metrics::section_body_padding); + frame_layout->setSpacing(0); + + if (scrollable) { + auto* scroll = new QScrollArea(frame); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + + auto* scroll_body = new QWidget(scroll); + scroll_body->setObjectName("panelScrollBody"); + body_layout_ = new QVBoxLayout(scroll_body); + body_layout_->setContentsMargins(0, 0, 0, 0); + body_layout_->setSpacing(0); + + scroll->setWidget(scroll_body); + frame_layout->addWidget(scroll); + } else { + auto* body = new QWidget(frame); + body_layout_ = new QVBoxLayout(body); + body_layout_->setContentsMargins(0, 0, 0, 0); + body_layout_->setSpacing(0); + frame_layout->addWidget(body); + } + + outer_layout->addWidget(frame); +} + +void Dialog::addBodyWidget(QWidget* widget) { + body_layout_->addWidget(widget); +} + +} // namespace ifcinterface::components diff --git a/src/interface/components/Dialog.h b/src/interface/components/Dialog.h new file mode 100644 index 0000000000..79a119adda --- /dev/null +++ b/src/interface/components/Dialog.h @@ -0,0 +1,46 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_COMPONENTS_DIALOG_H +#define IFCINTERFACE_COMPONENTS_DIALOG_H + +#include + +class QVBoxLayout; +class QWidget; + +namespace ifcinterface::components { + +class Dialog : public QDialog { + Q_OBJECT + +public: + explicit Dialog(QWidget* parent = nullptr, + bool scrollable = false); + + void addBodyWidget(QWidget* widget); + +private: + QVBoxLayout* body_layout_ = nullptr; +}; + +} // namespace ifcinterface::components + +#endif diff --git a/src/interface/components/Panel.cpp b/src/interface/components/Panel.cpp index 30ccebfa14..96bb2df3d9 100644 --- a/src/interface/components/Panel.cpp +++ b/src/interface/components/Panel.cpp @@ -95,15 +95,22 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s auto* scroll_body = new QWidget(scroll); scroll_body->setObjectName("panelScrollBody"); - auto* scroll_body_layout = new QVBoxLayout(scroll_body); - scroll_body_layout->setContentsMargins(0, 0, 0, 0); - scroll_body_layout->setSpacing(0); - scroll_body_layout->addWidget(content); + body_layout_ = new QVBoxLayout(scroll_body); + body_layout_->setContentsMargins(0, 0, 0, 0); + body_layout_->setSpacing(0); scroll->setWidget(scroll_body); frame_layout->addWidget(scroll); } else { - frame_layout->addWidget(content); + auto* body = new QWidget(frame); + body_layout_ = new QVBoxLayout(body); + body_layout_->setContentsMargins(0, 0, 0, 0); + body_layout_->setSpacing(0); + frame_layout->addWidget(body); + } + + if (content) { + body_layout_->addWidget(content); } outer_layout->addWidget(frame); @@ -115,4 +122,8 @@ Panel::Panel(const QString& title, QWidget* content, QWidget* parent, bool has_s setWidget(outer); } +void Panel::addBodyWidget(QWidget* widget) { + body_layout_->addWidget(widget); +} + } // namespace ifcinterface::components diff --git a/src/interface/components/Panel.h b/src/interface/components/Panel.h index b786f636e9..70767314f8 100644 --- a/src/interface/components/Panel.h +++ b/src/interface/components/Panel.h @@ -24,6 +24,7 @@ #include class QWidget; +class QVBoxLayout; namespace ifcinterface::components { @@ -32,10 +33,15 @@ class Panel : public QDockWidget { public: explicit Panel(const QString& title, - QWidget* content, + QWidget* content = nullptr, QWidget* parent = nullptr, bool has_settings = false, bool scrollable = false); + + void addBodyWidget(QWidget* widget); + +private: + QVBoxLayout* body_layout_ = nullptr; }; } // namespace ifcinterface::components diff --git a/src/interface/components/Style.cpp b/src/interface/components/Style.cpp index eb00179f96..e993871c0e 100644 --- a/src/interface/components/Style.cpp +++ b/src/interface/components/Style.cpp @@ -34,29 +34,39 @@ QString buildAppStyleSheet() { background: ${app_background}; color: ${primary_text}; } + QFileDialog, + QFileDialog QWidget, + QFileDialog QStackedWidget, + QFileDialog QSplitter { + background: ${app_background}; + color: ${primary_text}; + } QFrame#ribbonShell { background: ${ribbon_shell_background}; border-bottom: 1px solid ${border}; } - QTabBar::tab { + QTabBar#appTabBar { + background: ${ribbon_shell_background}; + } + QTabBar#appTabBar::tab { background: transparent; color: ${secondary_text}; padding: 8px 14px; margin-right: 2px; border-bottom: 2px solid transparent; } - QTabBar::tab:selected { + QTabBar#appTabBar::tab:selected { color: ${primary_text}; border-bottom: 2px solid ${selection_background}; } - QTabBar::tab:hover { + QTabBar#appTabBar::tab:hover { color: ${ribbon_tab_hover_text}; } QFrame#ribbonBand { background: ${ribbon_band_background}; border-top: 1px solid ${border}; } - QTabWidget::pane { + QTabWidget#appTabWidget::pane { border: none; background: transparent; } @@ -67,6 +77,9 @@ QString buildAppStyleSheet() { background: transparent; border-right: 1px solid ${border}; } + QFrame#ribbonGroup[separator="false"] { + border-right: none; + } QLabel#ribbonGroupLabel { font-size: 9px; font-weight: 600; @@ -105,6 +118,7 @@ QString buildAppStyleSheet() { QListWidget, QTableWidget, QLineEdit, + QComboBox, QSpinBox, QDoubleSpinBox, QCheckBox, @@ -179,6 +193,21 @@ QString buildAppStyleSheet() { QLineEdit:focus { border: 1px solid ${control_border_focus}; } + QComboBox { + background: ${control_background}; + border: 1px solid ${border}; + border-radius: ${panel_radius}px; + padding: ${padding}px ${padding}px; + } + QComboBox:focus { + border: 1px solid ${control_border_focus}; + } + QComboBox QAbstractItemView { + background: ${panel_background}; + border: 1px solid ${border}; + selection-background-color: ${selection_background}; + selection-color: ${selection_text}; + } QSpinBox, QDoubleSpinBox { background: ${control_background}; border: 1px solid ${border}; diff --git a/src/interface/components/Tabs.cpp b/src/interface/components/Tabs.cpp new file mode 100644 index 0000000000..86c4466a29 --- /dev/null +++ b/src/interface/components/Tabs.cpp @@ -0,0 +1,40 @@ +// 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 "Tabs.h" + +namespace ifcinterface::components { + +TabBar::TabBar(QWidget* parent) + : QTabBar(parent) +{ + setObjectName("appTabBar"); + setExpanding(false); + setDrawBase(false); +} + +TabWidget::TabWidget(QWidget* parent) + : QTabWidget(parent) +{ + setObjectName("appTabWidget"); + setTabBar(new TabBar(this)); +} + +} // namespace ifcinterface::components diff --git a/src/interface/components/Tabs.h b/src/interface/components/Tabs.h new file mode 100644 index 0000000000..ea70ce5f1c --- /dev/null +++ b/src/interface/components/Tabs.h @@ -0,0 +1,45 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_COMPONENTS_TABS_H +#define IFCINTERFACE_COMPONENTS_TABS_H + +#include +#include + +namespace ifcinterface::components { + +class TabBar : public QTabBar { + Q_OBJECT + +public: + explicit TabBar(QWidget* parent = nullptr); +}; + +class TabWidget : public QTabWidget { + Q_OBJECT + +public: + explicit TabWidget(QWidget* parent = nullptr); +}; + +} // namespace ifcinterface::components + +#endif diff --git a/src/interface/icons/cube-bandage.svg b/src/interface/icons/cube-bandage.svg new file mode 100644 index 0000000000..424f251a12 --- /dev/null +++ b/src/interface/icons/cube-bandage.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/interface/icons/database-restore.svg b/src/interface/icons/database-restore.svg new file mode 100644 index 0000000000..c3f1692043 --- /dev/null +++ b/src/interface/icons/database-restore.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/interface/icons/database.svg b/src/interface/icons/database.svg new file mode 100644 index 0000000000..686e9bdd6e --- /dev/null +++ b/src/interface/icons/database.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/interface/interface_resources.qrc b/src/interface/interface_resources.qrc index fcb8a40df5..feb89ef781 100644 --- a/src/interface/interface_resources.qrc +++ b/src/interface/interface_resources.qrc @@ -43,5 +43,8 @@ icons/sidebar-expand.svg icons/check.svg icons/xmark-circle.svg + icons/database.svg + icons/database-restore.svg + icons/cube-bandage.svg diff --git a/src/interface/main.cpp b/src/interface/main.cpp index 57e40f679c..353d587f01 100644 --- a/src/interface/main.cpp +++ b/src/interface/main.cpp @@ -19,6 +19,7 @@ ********************************************************************************/ #include "MainWindow.h" +#include "components/Style.h" #include #include @@ -70,6 +71,7 @@ int main(int argc, char* argv[]) { parser.process(app); installUiFont(); + app.setStyleSheet(ifcinterface::components::style::buildAppStyleSheet()); ifcinterface::shell::MainWindow window; window.show(); diff --git a/src/interface/panels/add_model/Dialog.cpp b/src/interface/panels/add_model/Dialog.cpp new file mode 100644 index 0000000000..1987cf123f --- /dev/null +++ b/src/interface/panels/add_model/Dialog.cpp @@ -0,0 +1,140 @@ +// 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 "Dialog.h" + +#include "../../components/Dialog.h" +#include "../../components/Buttons.h" +#include "../../components/Section.h" +#include "../../components/Style.h" + +#include +#include +#include +#include +#include + +namespace ifcinterface::panels::add_model { + +namespace { + +class HoverDescriptionFilter : public QObject { +public: + HoverDescriptionFilter(QLabel* label, QString hover_text, QString default_text) + : label_(label), hover_text_(std::move(hover_text)), default_text_(std::move(default_text)) {} + +protected: + bool eventFilter(QObject* watched, QEvent* event) override { + Q_UNUSED(watched); + if (event->type() == QEvent::Enter) { + label_->setText(hover_text_); + } else if (event->type() == QEvent::Leave) { + label_->setText(default_text_); + } + return false; + } + +private: + QLabel* label_ = nullptr; + QString hover_text_; + QString default_text_; +}; + +} // namespace + +AddModelDialog::AddModelDialog(QWidget* parent) + : components::Dialog(parent) +{ + setObjectName("appDialog"); + setWindowTitle("Add Model"); + setModal(true); + setStyleSheet(components::style::buildAppStyleSheet()); + setupUi(); +} + +void AddModelDialog::setupUi() { + if (auto* root = qobject_cast(layout())) { + root->setSizeConstraint(QLayout::SetFixedSize); + } + + const QString default_description = "Choose what to add to the project"; + auto* description_section = new components::Section("", components::SectionHeaderMode::Hidden, this); + auto* description = new QLabel(default_description, description_section); + description->setProperty("textRole", "secondary"); + description->setWordWrap(true); + description->setAlignment(Qt::AlignCenter); + description->setMinimumWidth((90 * 4) + (components::style::metrics::padding * 3)); + description->setMinimumHeight(description->fontMetrics().lineSpacing() * 2 + 4); + description_section->addBodyWidget(description); + + auto* choices_section = new components::Section("", components::SectionHeaderMode::Hidden, this); + auto* choices = new QWidget(choices_section); + auto* row = new QHBoxLayout(choices); + row->setContentsMargins(0, 0, 0, 0); + row->setSpacing(components::style::metrics::padding); + + auto* add_ifc = components::buttons::makeButton("Add IFC File", ":/icons/cube.svg", choices, QSize(90, 68)); + connect(add_ifc, &QToolButton::clicked, this, [this]() { + selected_mode_ = SourceMode::IfcFile; + accept(); + }); + add_ifc->installEventFilter(new HoverDescriptionFilter( + description, + "Add IFC files and load both geometry and data.", + default_description)); + + auto* add_database = components::buttons::makeButton("Add IFC\nDatabase", ":/icons/database.svg", choices, QSize(90, 68)); + connect(add_database, &QToolButton::clicked, this, [this]() { + selected_mode_ = SourceMode::IfcDatabase; + accept(); + }); + add_database->installEventFilter(new HoverDescriptionFilter( + description, + "Add IFC RDB databases for optimised performance", + default_description)); + + auto* add_geometry = components::buttons::makeButton("Add Geometry", ":/icons/cube-bandage.svg", choices, QSize(90, 68)); + connect(add_geometry, &QToolButton::clicked, this, [this]() { + selected_mode_ = SourceMode::GeometryOnly; + accept(); + }); + add_geometry->installEventFilter(new HoverDescriptionFilter( + description, + "Add pure geometry for fast visualisation", + default_description)); + + auto* convert_database = components::buttons::makeButton("Convert IFC File\nto Database", ":/icons/database-restore.svg", choices, QSize(90, 68)); + connect(convert_database, &QToolButton::clicked, this, [description]() { + description->setText("IFC-to-database conversion is coming soon."); + }); + convert_database->installEventFilter(new HoverDescriptionFilter( + description, + "Convert IFC files to databases for smaller filesizes, reduced memory, and faster access. No data is lost.", + default_description)); + + row->addWidget(components::buttons::makeButtonGroup("ADD", {add_ifc, add_database, add_geometry}, choices, true, 8)); + row->addWidget(components::buttons::makeButtonGroup("TOOLS", {convert_database}, choices, false, 8)); + choices_section->addBodyWidget(choices); + + addBodyWidget(description_section); + addBodyWidget(choices_section); +} + +} // namespace ifcinterface::panels::add_model diff --git a/src/interface/panels/add_model/Dialog.h b/src/interface/panels/add_model/Dialog.h new file mode 100644 index 0000000000..5e39e855de --- /dev/null +++ b/src/interface/panels/add_model/Dialog.h @@ -0,0 +1,50 @@ +// 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 . * + * * + ********************************************************************************/ + +#ifndef IFCINTERFACE_PANELS_ADDMODELDIALOG_H +#define IFCINTERFACE_PANELS_ADDMODELDIALOG_H + +#include "../../components/Dialog.h" + +namespace ifcinterface::panels::add_model { + +enum class SourceMode { + None, + IfcFile, + IfcDatabase, + GeometryOnly, +}; + +class AddModelDialog : public components::Dialog { + Q_OBJECT +public: + explicit AddModelDialog(QWidget* parent = nullptr); + + SourceMode selectedMode() const { return selected_mode_; } + +private: + void setupUi(); + + SourceMode selected_mode_ = SourceMode::None; +}; + +} // namespace ifcinterface::panels::add_model + +#endif diff --git a/src/interface/panels/properties/View.cpp b/src/interface/panels/properties/View.cpp index 3b58dc4ca9..c6535e3d80 100644 --- a/src/interface/panels/properties/View.cpp +++ b/src/interface/panels/properties/View.cpp @@ -40,6 +40,10 @@ PropertiesPanelView::PropertiesPanelView(PropertiesPanelWidget* widget, refresh(0); } +void PropertiesPanelView::clearSelection() { + refresh(0); +} + void PropertiesPanelView::refresh(uint32_t object_id) { PropertiesPanelState state; state.entity = {"IfcWall", "SOLIDWALL"}; diff --git a/src/interface/panels/properties/View.h b/src/interface/panels/properties/View.h index c38120fc5a..f68b1dfc45 100644 --- a/src/interface/panels/properties/View.h +++ b/src/interface/panels/properties/View.h @@ -38,6 +38,7 @@ public: ViewportWindow* viewport, ifcinterface::ElementRegistry* registry, QObject* parent = nullptr); + void clearSelection(); private: void refresh(uint32_t object_id); diff --git a/src/interface/panels/properties/Widget.cpp b/src/interface/panels/properties/Widget.cpp index 0afb0bc89f..316488c760 100644 --- a/src/interface/panels/properties/Widget.cpp +++ b/src/interface/panels/properties/Widget.cpp @@ -35,6 +35,13 @@ namespace { +void clearLayout(QVBoxLayout* layout) { + while (auto* item = layout->takeAt(0)) { + if (auto* widget = item->widget()) widget->deleteLater(); + delete item; + } +} + QWidget* makePropertySetPanel(const ifcinterface::panels::properties::PropertySet& property_set, QWidget* parent = nullptr) { auto* group = new QGroupBox(property_set.title, parent); group->setObjectName("propertySetBox"); @@ -91,6 +98,34 @@ QWidget* makeFilterWrapper(QLineEdit** field_out, QWidget* parent = nullptr) { return wrapper; } +QFrame* makeEntityBox(const ifcinterface::panels::properties::EntitySummary& entity, QWidget* parent = nullptr) { + auto* entity_box = new QFrame(parent); + entity_box->setObjectName("entityClassBox"); + auto* entity_layout = new QHBoxLayout(entity_box); + entity_layout->setContentsMargins(10, 8, 10, 8); + entity_layout->setSpacing(10); + + auto* entity_icon = new QLabel(entity_box); + entity_icon->setPixmap(ifcinterface::components::icons::makeSvgPixmap(":/icons/cube-dots.svg", QSize(28, 28))); + entity_icon->setAlignment(Qt::AlignCenter); + + auto* entity_text = new QWidget(entity_box); + auto* entity_text_layout = new QVBoxLayout(entity_text); + entity_text_layout->setContentsMargins(0, 0, 0, 0); + entity_text_layout->setSpacing(2); + + auto* entity_class_label = new QLabel(entity.entity_class, entity_text); + entity_class_label->setObjectName("entityClassLabel"); + auto* entity_type_label = new QLabel(entity.predefined_type, entity_text); + entity_type_label->setProperty("textRole", "secondary"); + + entity_text_layout->addWidget(entity_class_label); + entity_text_layout->addWidget(entity_type_label); + entity_layout->addWidget(entity_icon, 0, Qt::AlignVCenter); + entity_layout->addWidget(entity_text, 1, Qt::AlignVCenter); + return entity_box; +} + } // namespace namespace ifcinterface::panels::properties { @@ -101,93 +136,10 @@ PropertiesPanelWidget::PropertiesPanelWidget(QWidget* parent) content_layout_ = new QVBoxLayout(this); content_layout_->setContentsMargins(0, 0, 0, 0); content_layout_->setSpacing(12); - - auto* entity_box = new QFrame(this); - entity_box->setObjectName("entityClassBox"); - auto* entity_layout = new QHBoxLayout(entity_box); - entity_layout->setContentsMargins(10, 8, 10, 8); - entity_layout->setSpacing(10); - auto* entity_icon = new QLabel(entity_box); - entity_icon->setPixmap(components::icons::makeSvgPixmap(":/icons/cube-dots.svg", QSize(28, 28))); - entity_icon->setAlignment(Qt::AlignCenter); - auto* entity_text = new QWidget(entity_box); - auto* entity_text_layout = new QVBoxLayout(entity_text); - entity_text_layout->setContentsMargins(0, 0, 0, 0); - entity_text_layout->setSpacing(2); - entity_class_label_ = new QLabel(entity_text); - entity_class_label_->setObjectName("entityClassLabel"); - entity_type_label_ = new QLabel(entity_text); - entity_type_label_->setProperty("textRole", "secondary"); - entity_text_layout->addWidget(entity_class_label_); - entity_text_layout->addWidget(entity_type_label_); - entity_layout->addWidget(entity_icon, 0, Qt::AlignVCenter); - entity_layout->addWidget(entity_text, 1, Qt::AlignVCenter); - - entity_section_ = new components::Section("", components::SectionHeaderMode::Hidden, this); - entity_section_->addBodyWidget(entity_box); - attributes_section_ = new components::Section("Attributes", components::SectionHeaderMode::Visible, this); - relationships_section_ = new components::Section("Relationships", components::SectionHeaderMode::Visible, this); - properties_section_ = new components::Section("Properties", components::SectionHeaderMode::Visible, this); - quantities_section_ = new components::Section("Quantities", components::SectionHeaderMode::Visible, this); - - properties_filter_toggle_ = new QToolButton(properties_section_); - properties_filter_toggle_->setObjectName("panelSectionFilterToggle"); - properties_filter_toggle_->setCheckable(true); - properties_filter_toggle_->setIcon(components::icons::makeSvgIcon(":/icons/filter.svg")); - properties_filter_toggle_->setAutoRaise(true); - properties_section_->addHeaderWidget(properties_filter_toggle_); - - quantities_filter_toggle_ = new QToolButton(quantities_section_); - quantities_filter_toggle_->setObjectName("panelSectionFilterToggle"); - quantities_filter_toggle_->setCheckable(true); - quantities_filter_toggle_->setIcon(components::icons::makeSvgIcon(":/icons/filter.svg")); - quantities_filter_toggle_->setAutoRaise(true); - quantities_section_->addHeaderWidget(quantities_filter_toggle_); - - properties_filter_wrapper_ = makeFilterWrapper(&properties_filter_field_, properties_section_); - properties_filter_field_->setPlaceholderText("Filter properties or sets"); - quantities_filter_wrapper_ = makeFilterWrapper(&quantities_filter_field_, quantities_section_); - quantities_filter_field_->setPlaceholderText("Filter quantities or sets"); - - connect(properties_filter_toggle_, &QToolButton::toggled, properties_filter_field_, [this](bool visible) { - properties_filter_field_->setVisible(visible); - properties_filter_wrapper_->setVisible(visible); - if (visible) properties_filter_field_->setFocus(); - }); - connect(quantities_filter_toggle_, &QToolButton::toggled, quantities_filter_field_, [this](bool visible) { - quantities_filter_field_->setVisible(visible); - quantities_filter_wrapper_->setVisible(visible); - if (visible) quantities_filter_field_->setFocus(); - }); - - properties_filter_wrapper_->setVisible(false); - quantities_filter_wrapper_->setVisible(false); - - content_layout_->addWidget(entity_section_); - content_layout_->addWidget(attributes_section_); - content_layout_->addWidget(relationships_section_); - content_layout_->addWidget(properties_section_); - content_layout_->addWidget(quantities_section_); - content_layout_->addStretch(1); } void PropertiesPanelWidget::render(const PropertiesPanelState& state) { - const bool attributes_expanded = attributes_section_->isExpanded(); - const bool relationships_expanded = relationships_section_->isExpanded(); - const bool properties_expanded = properties_section_->isExpanded(); - const bool quantities_expanded = quantities_section_->isExpanded(); - const bool properties_filter_visible = properties_filter_toggle_->isChecked(); - const bool quantities_filter_visible = quantities_filter_toggle_->isChecked(); - const QString properties_filter_text = properties_filter_field_->text(); - const QString quantities_filter_text = quantities_filter_field_->text(); - - entity_class_label_->setText(state.entity.entity_class); - entity_type_label_->setText(state.entity.predefined_type); - - attributes_section_->clearBody(); - relationships_section_->clearBody(); - properties_section_->clearBody(); - quantities_section_->clearBody(); + clearLayout(content_layout_); QList property_set_widgets; for (const auto& property_set : state.property_sets) { @@ -199,21 +151,98 @@ void PropertiesPanelWidget::render(const PropertiesPanelState& state) { quantity_set_widgets.append(makePropertySetPanel(property_set, this)); } - attributes_section_->addBodyWidget(makeAttributeList(state.attributes, this)); - relationships_section_->addBodyWidget(makeRelationshipList(state.relationships, this)); - properties_section_->addBodyWidget(properties_filter_wrapper_); - for (auto* widget : property_set_widgets) properties_section_->addBodyWidget(widget); - quantities_section_->addBodyWidget(quantities_filter_wrapper_); - for (auto* widget : quantity_set_widgets) quantities_section_->addBodyWidget(widget); + auto* entity_section = new components::Section("", components::SectionHeaderMode::Hidden, this); + entity_section->addBodyWidget(makeEntityBox(state.entity, this)); - attributes_section_->setExpanded(attributes_expanded); - relationships_section_->setExpanded(relationships_expanded); - properties_section_->setExpanded(properties_expanded); - quantities_section_->setExpanded(quantities_expanded); - properties_filter_field_->setText(properties_filter_text); - quantities_filter_field_->setText(quantities_filter_text); - properties_filter_toggle_->setChecked(properties_filter_visible); - quantities_filter_toggle_->setChecked(quantities_filter_visible); + auto* attributes_section = new components::Section("Attributes", components::SectionHeaderMode::Visible, this); + attributes_section->addBodyWidget(makeAttributeList(state.attributes, this)); + attributes_section->setExpanded(attributes_expanded_); + + auto* relationships_section = new components::Section("Relationships", components::SectionHeaderMode::Visible, this); + relationships_section->addBodyWidget(makeRelationshipList(state.relationships, this)); + relationships_section->setExpanded(relationships_expanded_); + + auto* properties_section = new components::Section("Properties", components::SectionHeaderMode::Visible, this); + auto* properties_filter_toggle = new QToolButton(properties_section); + properties_filter_toggle->setObjectName("panelSectionFilterToggle"); + properties_filter_toggle->setCheckable(true); + properties_filter_toggle->setIcon(components::icons::makeSvgIcon(":/icons/filter.svg")); + properties_filter_toggle->setAutoRaise(true); + properties_section->addHeaderWidget(properties_filter_toggle); + QLineEdit* properties_filter_field = nullptr; + auto* properties_filter_wrapper = makeFilterWrapper(&properties_filter_field, properties_section); + properties_filter_field->setPlaceholderText("Filter properties or sets"); + properties_filter_field->setText(properties_filter_text_); + properties_filter_wrapper->setVisible(properties_filter_visible_); + properties_filter_field->setVisible(properties_filter_visible_); + connect(properties_filter_toggle, &QToolButton::toggled, properties_filter_field, [this, properties_filter_field, properties_filter_wrapper](bool visible) { + properties_filter_visible_ = visible; + properties_filter_field->setVisible(visible); + properties_filter_wrapper->setVisible(visible); + if (visible) properties_filter_field->setFocus(); + }); + connect(properties_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) { + properties_filter_text_ = text; + }); + properties_section->addBodyWidget(properties_filter_wrapper); + for (auto* widget : property_set_widgets) properties_section->addBodyWidget(widget); + properties_section->setExpanded(properties_expanded_); + properties_filter_toggle->setChecked(properties_filter_visible_); + + auto* quantities_section = new components::Section("Quantities", components::SectionHeaderMode::Visible, this); + auto* quantities_filter_toggle = new QToolButton(quantities_section); + quantities_filter_toggle->setObjectName("panelSectionFilterToggle"); + quantities_filter_toggle->setCheckable(true); + quantities_filter_toggle->setIcon(components::icons::makeSvgIcon(":/icons/filter.svg")); + quantities_filter_toggle->setAutoRaise(true); + quantities_section->addHeaderWidget(quantities_filter_toggle); + QLineEdit* quantities_filter_field = nullptr; + auto* quantities_filter_wrapper = makeFilterWrapper(&quantities_filter_field, quantities_section); + quantities_filter_field->setPlaceholderText("Filter quantities or sets"); + quantities_filter_field->setText(quantities_filter_text_); + quantities_filter_wrapper->setVisible(quantities_filter_visible_); + quantities_filter_field->setVisible(quantities_filter_visible_); + connect(quantities_filter_toggle, &QToolButton::toggled, quantities_filter_field, [this, quantities_filter_field, quantities_filter_wrapper](bool visible) { + quantities_filter_visible_ = visible; + quantities_filter_field->setVisible(visible); + quantities_filter_wrapper->setVisible(visible); + if (visible) quantities_filter_field->setFocus(); + }); + connect(quantities_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) { + quantities_filter_text_ = text; + }); + quantities_section->addBodyWidget(quantities_filter_wrapper); + for (auto* widget : quantity_set_widgets) quantities_section->addBodyWidget(widget); + quantities_section->setExpanded(quantities_expanded_); + quantities_filter_toggle->setChecked(quantities_filter_visible_); + + if (auto* button = attributes_section->findChild("panelSectionHeaderButton")) { + connect(button, &QToolButton::toggled, this, [this](bool expanded) { + attributes_expanded_ = expanded; + }); + } + if (auto* button = relationships_section->findChild("panelSectionHeaderButton")) { + connect(button, &QToolButton::toggled, this, [this](bool expanded) { + relationships_expanded_ = expanded; + }); + } + if (auto* button = properties_section->findChild("panelSectionHeaderButton")) { + connect(button, &QToolButton::toggled, this, [this](bool expanded) { + properties_expanded_ = expanded; + }); + } + if (auto* button = quantities_section->findChild("panelSectionHeaderButton")) { + connect(button, &QToolButton::toggled, this, [this](bool expanded) { + quantities_expanded_ = expanded; + }); + } + + content_layout_->addWidget(entity_section); + content_layout_->addWidget(attributes_section); + content_layout_->addWidget(relationships_section); + content_layout_->addWidget(properties_section); + content_layout_->addWidget(quantities_section); + content_layout_->addStretch(1); } } // namespace ifcinterface::panels::properties diff --git a/src/interface/panels/properties/Widget.h b/src/interface/panels/properties/Widget.h index f5dc78b19a..bc15e447ca 100644 --- a/src/interface/panels/properties/Widget.h +++ b/src/interface/panels/properties/Widget.h @@ -23,6 +23,7 @@ #include "Types.h" +#include #include class QVBoxLayout; @@ -42,19 +43,14 @@ public: private: QVBoxLayout* content_layout_ = nullptr; - QLabel* entity_class_label_ = nullptr; - QLabel* entity_type_label_ = nullptr; - components::Section* entity_section_ = nullptr; - components::Section* attributes_section_ = nullptr; - components::Section* relationships_section_ = nullptr; - components::Section* properties_section_ = nullptr; - components::Section* quantities_section_ = nullptr; - QWidget* properties_filter_wrapper_ = nullptr; - QWidget* quantities_filter_wrapper_ = nullptr; - QLineEdit* properties_filter_field_ = nullptr; - QLineEdit* quantities_filter_field_ = nullptr; - QToolButton* properties_filter_toggle_ = nullptr; - QToolButton* quantities_filter_toggle_ = nullptr; + bool attributes_expanded_ = true; + bool relationships_expanded_ = true; + bool properties_expanded_ = true; + bool quantities_expanded_ = true; + bool properties_filter_visible_ = false; + bool quantities_filter_visible_ = false; + QString properties_filter_text_; + QString quantities_filter_text_; }; } // namespace ifcinterface::panels::properties diff --git a/src/interface/panels/settings/Dialog.cpp b/src/interface/panels/settings/Dialog.cpp index e181d25db1..33c6ee75cb 100644 --- a/src/interface/panels/settings/Dialog.cpp +++ b/src/interface/panels/settings/Dialog.cpp @@ -21,9 +21,11 @@ #include "Dialog.h" #include "../../../ifcviewer/AppSettings.h" +#include "../../components/Dialog.h" #include "../../components/Section.h" #include "../../components/Style.h" #include "../../components/SvgIcon.h" +#include "../../components/Tabs.h" #include #include @@ -35,13 +37,12 @@ #include #include #include -#include #include namespace ifcinterface::panels::settings { SettingsDialog::SettingsDialog(QWidget* parent) - : QDialog(parent) + : components::Dialog(parent) { setObjectName("appDialog"); setWindowTitle("Settings"); @@ -57,23 +58,12 @@ void SettingsDialog::showEvent(QShowEvent* event) { } void SettingsDialog::setupUi() { - auto* root = new QVBoxLayout(this); - root->setContentsMargins(12, 12, 12, 12); - root->setSpacing(0); - - auto* panel = new QFrame(this); - panel->setObjectName("panel"); - auto* panel_layout = new QVBoxLayout(panel); - panel_layout->setContentsMargins(0, components::style::metrics::section_body_padding, 0, - components::style::metrics::section_body_padding); - panel_layout->setSpacing(12); - - auto* tabs = new QTabWidget(panel); + auto* tabs = new components::TabWidget(this); auto* graphics_tab = new QWidget(tabs); auto* graphics_layout = new QVBoxLayout(graphics_tab); graphics_layout->setContentsMargins(0, 0, 0, 0); - graphics_layout->setSpacing(12); + graphics_layout->setSpacing(components::style::metrics::padding); auto* general_section = new components::Section("General", components::SectionHeaderMode::Visible, graphics_tab); auto* general_body = new QWidget(general_section); @@ -148,7 +138,7 @@ void SettingsDialog::setupUi() { auto* tab = new QWidget(tabs); auto* layout = new QVBoxLayout(tab); layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(12); + layout->setSpacing(components::style::metrics::padding); auto* section = new components::Section(title, components::SectionHeaderMode::Visible, tab); auto* body = new QWidget(section); @@ -179,7 +169,7 @@ void SettingsDialog::setupUi() { tabs->addTab(make_placeholder_tab("About", "Version, credits, and environment information will live here."), "About"); - auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, panel); + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); if (auto* ok = buttons->button(QDialogButtonBox::Ok)) { ok->setText("OK"); ok->setIcon(components::icons::makeSvgIcon(":/icons/check.svg")); @@ -191,9 +181,11 @@ void SettingsDialog::setupUi() { connect(buttons, &QDialogButtonBox::accepted, this, &SettingsDialog::onAccepted); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - panel_layout->addWidget(tabs); - panel_layout->addWidget(buttons); - root->addWidget(panel); + auto* actions_section = new components::Section("", components::SectionHeaderMode::Hidden, this); + actions_section->addBodyWidget(buttons); + + addBodyWidget(tabs); + addBodyWidget(actions_section); } void SettingsDialog::syncFromSettings() { diff --git a/src/interface/panels/settings/Dialog.h b/src/interface/panels/settings/Dialog.h index 3b34ad8687..d97fab3db6 100644 --- a/src/interface/panels/settings/Dialog.h +++ b/src/interface/panels/settings/Dialog.h @@ -21,7 +21,7 @@ #ifndef IFCINTERFACE_PANELS_SETTINGSDIALOG_H #define IFCINTERFACE_PANELS_SETTINGSDIALOG_H -#include +#include "../../components/Dialog.h" class QCheckBox; class QDoubleSpinBox; @@ -31,7 +31,7 @@ class QSpinBox; namespace ifcinterface::panels::settings { -class SettingsDialog : public QDialog { +class SettingsDialog : public components::Dialog { Q_OBJECT public: explicit SettingsDialog(QWidget* parent = nullptr);