From b79070456f377ec659e7c35d57b23a6176c04222 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 11 May 2026 09:17:45 +1000 Subject: [PATCH] Interface mockup 15 --- src/ifcviewer/ViewportWindow.cpp | 14 +- src/ifcviewer/ViewportWindow.h | 3 + src/interface/CMakeLists.txt | 2 + src/interface/InterfaceSettings.cpp | 189 ++++++++++++++++++ src/interface/InterfaceSettings.h | 72 +++++++ src/interface/MainWindow.cpp | 1 - src/interface/components/Buttons.cpp | 2 +- src/interface/components/Dialog.cpp | 57 ++++++ src/interface/components/Dialog.h | 15 ++ src/interface/components/Style.cpp | 77 ++++--- src/interface/components/Style.h | 12 +- src/interface/components/SvgIcon.cpp | 18 +- src/interface/components/SvgIcon.h | 1 + src/interface/main.cpp | 10 +- .../modules/models/AddModelDialog.cpp | 1 - src/interface/modules/models/Panel.cpp | 4 + .../modules/models/SettingsDialog.cpp | 14 +- src/interface/modules/models/SettingsDialog.h | 2 +- src/interface/modules/settings/Dialog.cpp | 126 ++++++++++-- src/interface/modules/settings/Dialog.h | 17 +- src/interface/modules/viewport/Controller.cpp | 7 + 21 files changed, 556 insertions(+), 88 deletions(-) create mode 100644 src/interface/InterfaceSettings.cpp create mode 100644 src/interface/InterfaceSettings.h diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 76f18410cc..7d7deaec8f 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -725,7 +725,7 @@ void ViewportWindow::initGL() { gl_->glEnable(GL_DEPTH_TEST); gl_->glEnable(GL_MULTISAMPLE); - gl_->glClearColor(0.18f, 0.20f, 0.22f, 1.0f); + gl_->glClearColor(background_color_.redF(), background_color_.greenF(), background_color_.blueF(), 1.0f); gl_->glCullFace(GL_BACK); if (AppSettings::instance().backfaceCulling()) gl_->glEnable(GL_CULL_FACE); else gl_->glDisable(GL_CULL_FACE); @@ -768,6 +768,18 @@ void ViewportWindow::initGL() { emit initialized(); } +void ViewportWindow::setBackgroundColor(const QColor& color) { + if (!color.isValid()) return; + const QColor normalized = color.toRgb(); + if (background_color_ == normalized) return; + background_color_ = normalized; + if (gl_initialized_ && gl_) { + context_->makeCurrent(this); + gl_->glClearColor(background_color_.redF(), background_color_.greenF(), background_color_.blueF(), 1.0f); + requestUpdate(); + } +} + void ViewportWindow::enqueuePendingOperation(PendingOperation op) { pending_ops_.push_back(std::move(op)); } diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index e88e391c74..43ed4e8dae 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -312,6 +313,7 @@ public: void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters); void setModelCoordinateOperation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); void setModelTransformation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); + void setBackgroundColor(const QColor& color); // Selection. The viewport owns a SelectionState that tracks both // the multi-set and the "active" (last single-clicked) id. External @@ -607,6 +609,7 @@ private: QOpenGLContext* context_ = nullptr; QOpenGLFunctions_4_5_Core* gl_ = nullptr; bool gl_initialized_ = false; + QColor background_color_ = QColor("#202329"); std::deque pending_ops_; // Shaders diff --git a/src/interface/CMakeLists.txt b/src/interface/CMakeLists.txt index 469da99799..2b703edf4e 100644 --- a/src/interface/CMakeLists.txt +++ b/src/interface/CMakeLists.txt @@ -27,6 +27,8 @@ set(INTERFACE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ElementRegistry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ElementRegistry.h + ${CMAKE_CURRENT_SOURCE_DIR}/InterfaceSettings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/InterfaceSettings.h ${CMAKE_CURRENT_SOURCE_DIR}/SessionState.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SessionState.h ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp diff --git a/src/interface/InterfaceSettings.cpp b/src/interface/InterfaceSettings.cpp new file mode 100644 index 0000000000..5e05584c22 --- /dev/null +++ b/src/interface/InterfaceSettings.cpp @@ -0,0 +1,189 @@ +// 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 "InterfaceSettings.h" + +#include "components/Style.h" + +#include +#include + +namespace { + +constexpr const char* kThemeModeKey = "interface/theme/mode"; +constexpr const char* kThemeColorPrefix = "interface/theme/colors/"; + +using Spec = ifcinterface::InterfaceSettings::ThemeColorSpec; + +const std::vector kThemeColorSpecs = { + {"app_background", "App Background", ifcinterface::components::style::palette::app_background, "#eef1f5"}, + {"border", "Border", ifcinterface::components::style::palette::border, "#c9d1dc"}, + {"selection_background", "Selection Background", ifcinterface::components::style::palette::selection_background, + "#2f9e44"}, + {"tab_bar_background", "Tab Bar Background", ifcinterface::components::style::palette::tab_bar_background, + "#eef1f5"}, + {"tab_background", "Tab Background", ifcinterface::components::style::palette::tab_background, "#e3e8ef"}, + {"ribbon_background", "Ribbon Background", ifcinterface::components::style::palette::ribbon_background, + "#e3e8ef"}, + {"ribbon_button_hover", "Ribbon Button Hover", ifcinterface::components::style::palette::ribbon_button_hover, + "#d6dde7"}, + {"ribbon_button_pressed", "Ribbon Button Pressed", ifcinterface::components::style::palette::ribbon_button_pressed, + "#cad3df"}, + {"viewport_shell_background", "Viewport Shell Background", + ifcinterface::components::style::palette::viewport_shell_background, "#dbe1e8"}, + {"viewport_background", "Viewport Background", ifcinterface::components::style::palette::viewport_background, + "#eef2f6"}, + {"panel_background", "Panel Background", ifcinterface::components::style::palette::panel_background, "#ffffff"}, + {"control_background", "Control Background", ifcinterface::components::style::palette::control_background, + "#f5f7fa"}, + {"control_border_focus", "Control Border Focus", ifcinterface::components::style::palette::control_border_focus, + "#7c8ca3"}, + {"box_background", "Box Background", ifcinterface::components::style::palette::box_background, "#f5f7fa"}, + {"scroll_handle", "Scroll Handle", ifcinterface::components::style::palette::scroll_handle, "#b1bac8"}, + {"scroll_handle_hover", "Scroll Handle Hover", ifcinterface::components::style::palette::scroll_handle_hover, + "#929daf"}, + {"status_background", "Status Background", ifcinterface::components::style::palette::status_background, + "#edf1f5"}, + {"section_header_background", "Section Header Background", + ifcinterface::components::style::palette::section_header_background, "#eef2f6"}, + {"primary_text", "Primary Text", ifcinterface::components::style::palette::primary_text, "#1f2937"}, + {"secondary_text", "Secondary Text", ifcinterface::components::style::palette::secondary_text, "#5b6676"}, + {"disabled_text", "Disabled Text", ifcinterface::components::style::palette::disabled_text, "#8b95a3"}, + {"warning_text", "Warning Text", ifcinterface::components::style::palette::warning_text, "#b26b00"}, + {"selection_text", "Selection Text", ifcinterface::components::style::palette::selection_text, "#ffffff"}, + {"hover_text", "Hover Text", ifcinterface::components::style::palette::hover_text, "#0f172a"}, + {"icon_color", "Icon Color", "#e7ebf2", "#445066"}, + {"icon_active_color", "Icon Active Color", "#ffffff", "#101828"}, + {"icon_disabled_color", "Icon Disabled Color", "#6f7988", "#98a2b3"}, + {"icon_accent_color", "Accent Icon Color", "#39b54a", "#2f9e44"}, + {"icon_accent_active_color", "Accent Icon Active Color", "#53c763", "#267e37"}, +}; + +int colorIndexForKey(const QString& key) { + for (size_t i = 0; i < kThemeColorSpecs.size(); ++i) { + if (QString::fromUtf8(kThemeColorSpecs[i].key) == key) { + return static_cast(i); + } + } + return -1; +} + +QString normalizedColor(const QString& value, const QString& fallback) { + const QString trimmed = value.trimmed(); + if (!QColor::isValidColorName(trimmed)) return fallback; + return QColor(trimmed).name(QColor::HexRgb); +} + +} // namespace + +namespace ifcinterface { + +InterfaceSettings& InterfaceSettings::instance() { + static InterfaceSettings inst; + return inst; +} + +const std::vector& InterfaceSettings::themeColorSpecs() { + return kThemeColorSpecs; +} + +InterfaceSettings::InterfaceSettings() { + custom_colors_.resize(kThemeColorSpecs.size()); + load(); +} + +InterfaceSettings::ThemeMode InterfaceSettings::themeMode() const { + return theme_mode_; +} + +void InterfaceSettings::setThemeMode(ThemeMode mode) { + if (theme_mode_ == mode) return; + theme_mode_ = mode; + persist(); + emit themeModeChanged(mode); + emit themeChanged(); +} + +QString InterfaceSettings::color(const QString& key) const { + const int index = colorIndexForKey(key); + if (index < 0) return {}; + + const auto& spec = kThemeColorSpecs[static_cast(index)]; + switch (theme_mode_) { + case ThemeMode::Dark: + return QString::fromUtf8(spec.dark_default); + case ThemeMode::Light: + return QString::fromUtf8(spec.light_default); + case ThemeMode::Custom: + return customColor(key); + } + return QString::fromUtf8(spec.dark_default); +} + +QString InterfaceSettings::customColor(const QString& key) const { + const int index = colorIndexForKey(key); + if (index < 0) return {}; + const auto& spec = kThemeColorSpecs[static_cast(index)]; + const QString& stored = custom_colors_[static_cast(index)]; + return stored.isEmpty() ? QString::fromUtf8(spec.dark_default) : stored; +} + +void InterfaceSettings::setCustomColor(const QString& key, const QString& value) { + const int index = colorIndexForKey(key); + if (index < 0) return; + + const auto& spec = kThemeColorSpecs[static_cast(index)]; + const QString normalized = normalizedColor(value, QString::fromUtf8(spec.dark_default)); + QString& stored = custom_colors_[static_cast(index)]; + if (stored == normalized) return; + stored = normalized; + persist(); + if (theme_mode_ == ThemeMode::Custom) { + emit themeChanged(); + } +} + +void InterfaceSettings::load() { + QSettings settings; + int raw_mode = settings.value(kThemeModeKey, static_cast(ThemeMode::Dark)).toInt(); + if (raw_mode < static_cast(ThemeMode::Dark) || raw_mode > static_cast(ThemeMode::Custom)) { + raw_mode = static_cast(ThemeMode::Dark); + } + theme_mode_ = static_cast(raw_mode); + + for (size_t i = 0; i < kThemeColorSpecs.size(); ++i) { + const auto& spec = kThemeColorSpecs[i]; + const QString stored = settings.value(QString::fromUtf8(kThemeColorPrefix) + QString::fromUtf8(spec.key), + QString::fromUtf8(spec.dark_default)) + .toString(); + custom_colors_[i] = normalizedColor(stored, QString::fromUtf8(spec.dark_default)); + } +} + +void InterfaceSettings::persist() const { + QSettings settings; + settings.setValue(kThemeModeKey, static_cast(theme_mode_)); + for (size_t i = 0; i < kThemeColorSpecs.size(); ++i) { + settings.setValue(QString::fromUtf8(kThemeColorPrefix) + QString::fromUtf8(kThemeColorSpecs[i].key), + custom_colors_[i]); + } +} + +} // namespace ifcinterface diff --git a/src/interface/InterfaceSettings.h b/src/interface/InterfaceSettings.h new file mode 100644 index 0000000000..1079e6c514 --- /dev/null +++ b/src/interface/InterfaceSettings.h @@ -0,0 +1,72 @@ +// 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_INTERFACESETTINGS_H +#define IFCINTERFACE_INTERFACESETTINGS_H + +#include +#include +#include + +namespace ifcinterface { + +class InterfaceSettings : public QObject { + Q_OBJECT +public: + enum class ThemeMode { + Dark = 0, + Light = 1, + Custom = 2, + }; + Q_ENUM(ThemeMode) + + struct ThemeColorSpec { + const char* key; + const char* label; + const char* dark_default; + const char* light_default; + }; + + static InterfaceSettings& instance(); + static const std::vector& themeColorSpecs(); + + ThemeMode themeMode() const; + void setThemeMode(ThemeMode mode); + + QString color(const QString& key) const; + QString customColor(const QString& key) const; + void setCustomColor(const QString& key, const QString& value); + +signals: + void themeModeChanged(ThemeMode mode); + void themeChanged(); + +private: + InterfaceSettings(); + void load(); + void persist() const; + + ThemeMode theme_mode_ = ThemeMode::Dark; + std::vector custom_colors_; +}; + +} // namespace ifcinterface + +#endif diff --git a/src/interface/MainWindow.cpp b/src/interface/MainWindow.cpp index 3dad893ab6..4884d6481a 100644 --- a/src/interface/MainWindow.cpp +++ b/src/interface/MainWindow.cpp @@ -299,7 +299,6 @@ QWidget* MainWindow::buildPanelsRibbonPage() { void MainWindow::setupRibbon() { auto* shell = new QFrame(this); - shell->setObjectName("ribbonShell"); auto* shell_layout = new QVBoxLayout(shell); shell_layout->setContentsMargins(0, 0, 0, 0); diff --git a/src/interface/components/Buttons.cpp b/src/interface/components/Buttons.cpp index 353963d496..3420f81f25 100644 --- a/src/interface/components/Buttons.cpp +++ b/src/interface/components/Buttons.cpp @@ -36,7 +36,7 @@ QToolButton* makeButton(const QString& text, auto* button = new QToolButton(parent); button->setObjectName("ribbonButton"); button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); - button->setIcon(components::icons::makeTintedSvgIcon(icon_path)); + button->setIcon(components::icons::makeAccentSvgIcon(icon_path)); button->setIconSize(QSize(20, 20)); button->setText(text); button->setMinimumSize(QSize(90, 68)); diff --git a/src/interface/components/Dialog.cpp b/src/interface/components/Dialog.cpp index 9f1c9cdde5..01c6d3e049 100644 --- a/src/interface/components/Dialog.cpp +++ b/src/interface/components/Dialog.cpp @@ -21,6 +21,7 @@ #include "Dialog.h" #include "Style.h" +#include "Tabs.h" #include #include @@ -83,4 +84,60 @@ void Dialog::addFooterWidget(QWidget* widget) { footer_layout_->addWidget(widget); } +TabbedDialog::TabbedDialog(QWidget* parent) + : 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); + + tabs_ = new TabWidget(frame); + frame_layout->addWidget(tabs_, 1); + + auto* footer = new QWidget(frame); + footer_layout_ = new QVBoxLayout(footer); + footer_layout_->setContentsMargins(style::metrics::section_body_padding, + style::metrics::section_body_padding, + style::metrics::section_body_padding, + 0); + footer_layout_->setSpacing(style::metrics::section_body_padding); + footer_layout_->setAlignment(Qt::AlignTop); + frame_layout->addWidget(footer); + + outer_layout->addWidget(frame); +} + +void TabbedDialog::addTab(const QString& title, QWidget* widget) { + auto* scroll = new QScrollArea(tabs_); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + + auto* scroll_body = new QWidget(scroll); + scroll_body->setObjectName("panelScrollBody"); + auto* layout = new QVBoxLayout(scroll_body); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(style::metrics::section_body_padding); + layout->setAlignment(Qt::AlignTop); + layout->addWidget(widget); + + scroll->setWidget(scroll_body); + tabs_->addTab(scroll, title); +} + +void TabbedDialog::addFooterWidget(QWidget* widget) { + footer_layout_->addWidget(widget); +} + } // namespace ifcinterface::components diff --git a/src/interface/components/Dialog.h b/src/interface/components/Dialog.h index 9145cce428..09959cf7c1 100644 --- a/src/interface/components/Dialog.h +++ b/src/interface/components/Dialog.h @@ -25,6 +25,7 @@ class QVBoxLayout; class QWidget; +class QTabWidget; namespace ifcinterface::components { @@ -43,6 +44,20 @@ private: QVBoxLayout* footer_layout_ = nullptr; }; +class TabbedDialog : public QDialog { + Q_OBJECT + +public: + explicit TabbedDialog(QWidget* parent = nullptr); + + void addTab(const QString& title, QWidget* widget); + void addFooterWidget(QWidget* widget); + +private: + QTabWidget* tabs_ = nullptr; + QVBoxLayout* footer_layout_ = nullptr; +}; + } // namespace ifcinterface::components #endif diff --git a/src/interface/components/Style.cpp b/src/interface/components/Style.cpp index f9ce108713..5c6e282d03 100644 --- a/src/interface/components/Style.cpp +++ b/src/interface/components/Style.cpp @@ -20,9 +20,12 @@ #include "Style.h" +#include "../InterfaceSettings.h" + namespace ifcinterface::components::style { QString buildAppStyleSheet() { + const auto& theme = ifcinterface::InterfaceSettings::instance(); QString stylesheet = QStringLiteral(R"( QMainWindow#appWindow { background: ${app_background}; @@ -47,15 +50,11 @@ QString buildAppStyleSheet() { background: ${app_background}; color: ${primary_text}; } - QFrame#ribbonShell { - background: ${ribbon_shell_background}; - border-bottom: 1px solid ${border}; - } QTabBar#appTabBar { - background: ${ribbon_shell_background}; + background: ${tab_bar_background}; } QTabBar#appTabBar::tab { - background: transparent; + background: ${tab_background}; color: ${secondary_text}; padding: 8px 14px; margin-right: 2px; @@ -66,10 +65,10 @@ QString buildAppStyleSheet() { border-bottom: 2px solid ${selection_background}; } QTabBar#appTabBar::tab:hover { - color: ${ribbon_tab_hover_text}; + color: ${hover_text}; } QFrame#ribbonBand { - background: ${ribbon_band_background}; + background: ${ribbon_background}; border-top: 1px solid ${border}; } QTabWidget#appTabWidget::pane { @@ -107,7 +106,6 @@ QString buildAppStyleSheet() { border-top: none; } QFrame#viewportFrame { - background: ${viewport_background}; border: 1px solid ${border}; } QDockWidget { @@ -146,13 +144,12 @@ QString buildAppStyleSheet() { font-weight: 600; } QToolButton#panelTitleButton { - color: ${panel_title_button}; border: none; background: transparent; } QToolButton#panelTitleButton:hover { - color: ${ribbon_tab_hover_text}; - background: ${panel_title_button_hover}; + color: ${hover_text}; + background: ${ribbon_button_hover}; } QFrame#panel { background: ${panel_background}; @@ -440,7 +437,7 @@ QString buildAppStyleSheet() { margin: 0; } QToolButton#panelSectionHeaderButton:hover { - color: ${ribbon_tab_hover_text}; + color: ${hover_text}; } QToolButton#panelSectionHeaderButton::menu-indicator { image: none; @@ -452,13 +449,13 @@ QString buildAppStyleSheet() { padding: ${section_header_padding}px; } QToolButton#panelSectionFilterToggle:hover { - background: ${panel_title_button_hover}; + background: ${ribbon_button_hover}; } QWidget#panelSectionBody { background: transparent; } QLabel#keyValueValueLabel { - color: ${key_value_value_text}; + color: ${primary_text}; background: transparent; } )"); @@ -468,33 +465,31 @@ QString buildAppStyleSheet() { stylesheet.replace("${section_header_padding}", QString::number(metrics::section_header_padding)); stylesheet.replace("${font_small}", QString::number(typography::small)); - stylesheet.replace("${app_background}", palette::app_background); - stylesheet.replace("${border}", palette::border); - stylesheet.replace("${selection_background}", palette::selection_background); - stylesheet.replace("${selection_text}", palette::selection_text); - stylesheet.replace("${ribbon_shell_background}", palette::ribbon_shell_background); - stylesheet.replace("${ribbon_tab_hover_text}", palette::ribbon_tab_hover_text); - stylesheet.replace("${ribbon_band_background}", palette::ribbon_band_background); - stylesheet.replace("${ribbon_button_hover}", palette::ribbon_button_hover); - stylesheet.replace("${ribbon_button_pressed}", palette::ribbon_button_pressed); - stylesheet.replace("${viewport_shell_background}", palette::viewport_shell_background); - stylesheet.replace("${viewport_background}", palette::viewport_background); - stylesheet.replace("${panel_title_button}", palette::panel_title_button); - stylesheet.replace("${panel_title_button_hover}", palette::panel_title_button_hover); - stylesheet.replace("${panel_background}", palette::panel_background); - stylesheet.replace("${control_background}", palette::control_background); - stylesheet.replace("${control_border_focus}", palette::control_border_focus); - stylesheet.replace("${box_background}", palette::box_background); - stylesheet.replace("${scroll_handle}", palette::scroll_handle); - stylesheet.replace("${scroll_handle_hover}", palette::scroll_handle_hover); - stylesheet.replace("${status_background}", palette::status_background); - stylesheet.replace("${section_header_background}", palette::section_header_background); - stylesheet.replace("${key_value_value_text}", palette::key_value_value_text); + stylesheet.replace("${app_background}", theme.color("app_background")); + stylesheet.replace("${border}", theme.color("border")); + stylesheet.replace("${selection_background}", theme.color("selection_background")); + stylesheet.replace("${selection_text}", theme.color("selection_text")); + stylesheet.replace("${tab_bar_background}", theme.color("tab_bar_background")); + stylesheet.replace("${tab_background}", theme.color("tab_background")); + stylesheet.replace("${hover_text}", theme.color("hover_text")); + stylesheet.replace("${ribbon_background}", theme.color("ribbon_background")); + stylesheet.replace("${ribbon_button_hover}", theme.color("ribbon_button_hover")); + stylesheet.replace("${ribbon_button_pressed}", theme.color("ribbon_button_pressed")); + stylesheet.replace("${viewport_shell_background}", theme.color("viewport_shell_background")); + stylesheet.replace("${viewport_background}", theme.color("viewport_background")); + stylesheet.replace("${panel_background}", theme.color("panel_background")); + stylesheet.replace("${control_background}", theme.color("control_background")); + stylesheet.replace("${control_border_focus}", theme.color("control_border_focus")); + stylesheet.replace("${box_background}", theme.color("box_background")); + stylesheet.replace("${scroll_handle}", theme.color("scroll_handle")); + stylesheet.replace("${scroll_handle_hover}", theme.color("scroll_handle_hover")); + stylesheet.replace("${status_background}", theme.color("status_background")); + stylesheet.replace("${section_header_background}", theme.color("section_header_background")); - stylesheet.replace("${primary_text}", palette::primary_text); - stylesheet.replace("${secondary_text}", palette::secondary_text); - stylesheet.replace("${disabled_text}", palette::disabled_text); - stylesheet.replace("${warning_text}", palette::warning_text); + stylesheet.replace("${primary_text}", theme.color("primary_text")); + stylesheet.replace("${secondary_text}", theme.color("secondary_text")); + stylesheet.replace("${disabled_text}", theme.color("disabled_text")); + stylesheet.replace("${warning_text}", theme.color("warning_text")); return stylesheet; } diff --git a/src/interface/components/Style.h b/src/interface/components/Style.h index 3dc1dd0b20..a72b42ec80 100644 --- a/src/interface/components/Style.h +++ b/src/interface/components/Style.h @@ -43,15 +43,13 @@ namespace ifcinterface::components::style::palette { inline constexpr auto app_background = "#26292f"; inline constexpr auto border = "#3e444e"; inline constexpr auto selection_background = "#39b54a"; -inline constexpr auto ribbon_shell_background = "#2d3138"; -inline constexpr auto ribbon_tab_hover_text = "#ffffff"; -inline constexpr auto ribbon_band_background = "#31353d"; +inline constexpr auto tab_bar_background = "#26292f"; +inline constexpr auto tab_background = "#31353d"; +inline constexpr auto ribbon_background = "#31353d"; inline constexpr auto ribbon_button_hover = "#3a3f48"; inline constexpr auto ribbon_button_pressed = "#24282f"; inline constexpr auto viewport_shell_background = "#202329"; -inline constexpr auto viewport_background = "#1a1d22"; -inline constexpr auto panel_title_button = "#8e97a5"; -inline constexpr auto panel_title_button_hover = "#353a42"; +inline constexpr auto viewport_background = "#2e3338"; inline constexpr auto panel_background = "#2b2f36"; inline constexpr auto control_background = "#31353d"; inline constexpr auto control_border_focus = "#5b6472"; @@ -60,13 +58,13 @@ inline constexpr auto scroll_handle = "#525a67"; inline constexpr auto scroll_handle_hover = "#697385"; inline constexpr auto status_background = "#26292f"; inline constexpr auto section_header_background = "#26292f"; -inline constexpr auto key_value_value_text = "#dce2eb"; inline constexpr auto primary_text = "#d0d5dd"; inline constexpr auto secondary_text = "#9aa4b3"; inline constexpr auto disabled_text = "#8f98a6"; inline constexpr auto warning_text = "#e4b35a"; inline constexpr auto selection_text = "#14161a"; +inline constexpr auto hover_text = "#ffffff"; } // namespace ifcinterface::components::style::palette diff --git a/src/interface/components/SvgIcon.cpp b/src/interface/components/SvgIcon.cpp index 2c6470f240..b2e70866c4 100644 --- a/src/interface/components/SvgIcon.cpp +++ b/src/interface/components/SvgIcon.cpp @@ -20,6 +20,8 @@ #include "SvgIcon.h" +#include "../InterfaceSettings.h" + #include #include #include @@ -61,12 +63,24 @@ QIcon makeTintedSvgIcon(const QString& icon_path, const QString& normal, return icon; } +QIcon makeAccentSvgIcon(const QString& icon_path) { + const auto& theme = ifcinterface::InterfaceSettings::instance(); + return makeTintedSvgIcon(icon_path, + theme.color("icon_accent_color"), + theme.color("icon_accent_active_color"), + theme.color("icon_disabled_color")); +} + QIcon makeSvgIcon(const QString& icon_path) { - return makeTintedSvgIcon(icon_path, "#e7ebf2", "#ffffff", "#6f7988"); + const auto& theme = ifcinterface::InterfaceSettings::instance(); + return makeTintedSvgIcon(icon_path, + theme.color("icon_color"), + theme.color("icon_active_color"), + theme.color("icon_disabled_color")); } QPixmap makeSvgPixmap(const QString& icon_path, const QSize& size) { - return renderTintedSvgPixmap(icon_path, "#e7ebf2", size); + return renderTintedSvgPixmap(icon_path, ifcinterface::InterfaceSettings::instance().color("icon_color"), size); } } // namespace ifcinterface::components::icons diff --git a/src/interface/components/SvgIcon.h b/src/interface/components/SvgIcon.h index c8d3bb949a..126f85be7e 100644 --- a/src/interface/components/SvgIcon.h +++ b/src/interface/components/SvgIcon.h @@ -33,6 +33,7 @@ QIcon makeTintedSvgIcon(const QString& icon_path, const QString& normal = "#39b54a", const QString& active = "#53c763", const QString& disabled = "#6f7988"); +QIcon makeAccentSvgIcon(const QString& icon_path); QIcon makeSvgIcon(const QString& icon_path); QPixmap makeSvgPixmap(const QString& icon_path, const QSize& size); diff --git a/src/interface/main.cpp b/src/interface/main.cpp index 91db90afd5..3c574fef1f 100644 --- a/src/interface/main.cpp +++ b/src/interface/main.cpp @@ -19,6 +19,7 @@ ********************************************************************************/ #include "MainWindow.h" +#include "InterfaceSettings.h" #include "components/Style.h" #include @@ -65,7 +66,14 @@ int main(int argc, char* argv[]) { parser.process(app); installUiFont(); - app.setStyleSheet(ifcinterface::components::style::buildAppStyleSheet()); + const auto applyStyle = [&app]() { + app.setStyleSheet(ifcinterface::components::style::buildAppStyleSheet()); + }; + applyStyle(); + QObject::connect(&ifcinterface::InterfaceSettings::instance(), + &ifcinterface::InterfaceSettings::themeChanged, + &app, + applyStyle); ifcinterface::shell::MainWindow window; window.show(); diff --git a/src/interface/modules/models/AddModelDialog.cpp b/src/interface/modules/models/AddModelDialog.cpp index 5976c12bfd..a5a80f07f8 100644 --- a/src/interface/modules/models/AddModelDialog.cpp +++ b/src/interface/modules/models/AddModelDialog.cpp @@ -65,7 +65,6 @@ AddModelDialog::AddModelDialog(QWidget* parent) setObjectName("appDialog"); setWindowTitle("Add Model"); setModal(true); - setStyleSheet(components::style::buildAppStyleSheet()); setupUi(); } diff --git a/src/interface/modules/models/Panel.cpp b/src/interface/modules/models/Panel.cpp index 47b0e50efa..fc60f7ecc8 100644 --- a/src/interface/modules/models/Panel.cpp +++ b/src/interface/modules/models/Panel.cpp @@ -107,6 +107,10 @@ ModelsPanel::ModelsPanel(QWidget* parent) void ModelsPanel::setNodes(const QList& 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); } diff --git a/src/interface/modules/models/SettingsDialog.cpp b/src/interface/modules/models/SettingsDialog.cpp index 8754c69d5a..8935143f97 100644 --- a/src/interface/modules/models/SettingsDialog.cpp +++ b/src/interface/modules/models/SettingsDialog.cpp @@ -136,7 +136,7 @@ QLabel* makeReadOnlyValue(QWidget* parent) { } // namespace SettingsDialog::SettingsDialog(ifcinterface::SessionState* session_state, QWidget* parent) - : components::Dialog(parent, true) + : components::TabbedDialog(parent) , session_state_(session_state) , federation_(session_state ? session_state->federation() : nullptr) , loader_(session_state ? session_state->loader() : nullptr) @@ -145,7 +145,6 @@ SettingsDialog::SettingsDialog(ifcinterface::SessionState* session_state, QWidge setWindowTitle("Model Settings"); setModal(true); resize(980, 560); - setStyleSheet(components::style::buildAppStyleSheet()); setupUi(); } @@ -158,9 +157,7 @@ void SettingsDialog::showEvent(QShowEvent* event) { } void SettingsDialog::setupUi() { - auto* tabs = new components::TabWidget(this); - - auto* federation_tab = new QWidget(tabs); + auto* federation_tab = new QWidget(this); auto* federation_layout = new QVBoxLayout(federation_tab); federation_layout->setContentsMargins(0, 0, 0, 0); federation_layout->setSpacing(components::style::metrics::padding); @@ -213,7 +210,7 @@ void SettingsDialog::setupUi() { federation_layout->addWidget(origin_section); federation_layout->addStretch(1); - auto* model_tab = new QWidget(tabs); + auto* model_tab = new QWidget(this); auto* model_layout = new QVBoxLayout(model_tab); model_layout->setContentsMargins(0, 0, 0, 0); model_layout->setSpacing(components::style::metrics::padding); @@ -324,8 +321,8 @@ void SettingsDialog::setupUi() { model_layout->addWidget(georef_section); model_layout->addWidget(table_section); - tabs->addTab(federation_tab, "Federation"); - tabs->addTab(model_tab, "Model"); + addTab("Federation", federation_tab); + addTab("Model", model_tab); connect(model_table_, &QTableWidget::currentCellChanged, this, [this](int /*current_row*/, int /*current_column*/, int /*previous_row*/, int /*previous_column*/) { @@ -344,7 +341,6 @@ void SettingsDialog::setupUi() { connect(buttons, &QDialogButtonBox::accepted, this, [this]() { onAccepted(); }); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - addBodyWidget(tabs); addFooterWidget(buttons); } diff --git a/src/interface/modules/models/SettingsDialog.h b/src/interface/modules/models/SettingsDialog.h index 4ad19f320f..4c3170998d 100644 --- a/src/interface/modules/models/SettingsDialog.h +++ b/src/interface/modules/models/SettingsDialog.h @@ -41,7 +41,7 @@ class SessionState; namespace ifcinterface::modules::models { -class SettingsDialog : public components::Dialog { +class SettingsDialog : public components::TabbedDialog { Q_OBJECT public: explicit SettingsDialog(ifcinterface::SessionState* session_state, QWidget* parent = nullptr); diff --git a/src/interface/modules/settings/Dialog.cpp b/src/interface/modules/settings/Dialog.cpp index 6da4ac7b6e..6eb8af5830 100644 --- a/src/interface/modules/settings/Dialog.cpp +++ b/src/interface/modules/settings/Dialog.cpp @@ -20,19 +20,21 @@ #include "Dialog.h" +#include "../../InterfaceSettings.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 "../../components/Style.h" #include +#include #include #include #include #include #include +#include #include #include #include @@ -43,13 +45,12 @@ namespace ifcinterface::modules::settings { SettingsDialog::SettingsDialog(QWidget* parent) - : components::Dialog(parent) + : components::TabbedDialog(parent) { setObjectName("appDialog"); setWindowTitle("Settings"); setModal(true); resize(520, 420); - setStyleSheet(components::style::buildAppStyleSheet()); setupUi(); } @@ -59,12 +60,11 @@ void SettingsDialog::showEvent(QShowEvent* event) { } void SettingsDialog::setupUi() { - auto* tabs = new components::TabWidget(this); - - auto* graphics_tab = new QWidget(tabs); + auto* graphics_tab = new QWidget(this); auto* graphics_layout = new QVBoxLayout(graphics_tab); graphics_layout->setContentsMargins(0, 0, 0, 0); graphics_layout->setSpacing(components::style::metrics::padding); + graphics_layout->setAlignment(Qt::AlignTop); auto* general_section = new components::Section("General", components::SectionHeaderMode::Visible, graphics_tab); auto* general_body = new QWidget(general_section); @@ -159,14 +159,15 @@ void SettingsDialog::setupUi() { // Navigation tab: orbit / pan presets. Selection always stays on // LMB so click + box-select keep working regardless of preset. - auto* navigation_tab = new QWidget(tabs); + auto* interface_tab = new QWidget(this); { - auto* layout = new QVBoxLayout(navigation_tab); + auto* layout = new QVBoxLayout(interface_tab); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(components::style::metrics::padding); + layout->setAlignment(Qt::AlignTop); auto* section = new components::Section( - "Navigation", components::SectionHeaderMode::Visible, navigation_tab); + "Navigation", components::SectionHeaderMode::Visible, interface_tab); auto* body = new QWidget(section); auto* form = new QFormLayout(body); form->setContentsMargins(0, 0, 0, 0); @@ -187,14 +188,63 @@ void SettingsDialog::setupUi() { section->addBodyWidget(body); layout->addWidget(section); - layout->addStretch(1); + + auto* theme_section = new components::Section( + "Theme", components::SectionHeaderMode::Visible, interface_tab); + auto* theme_body = new QWidget(theme_section); + auto* theme_layout = new QVBoxLayout(theme_body); + theme_layout->setContentsMargins(0, 0, 0, 0); + theme_layout->setSpacing(components::style::metrics::padding); + + auto* theme_form = new QFormLayout(); + theme_form->setContentsMargins(0, 0, 0, 0); + theme_form->setHorizontalSpacing(16); + theme_form->setVerticalSpacing(10); + + theme_mode_combo_ = new QComboBox(theme_body); + theme_mode_combo_->addItem("Dark", static_cast(ifcinterface::InterfaceSettings::ThemeMode::Dark)); + theme_mode_combo_->addItem("Light", static_cast(ifcinterface::InterfaceSettings::ThemeMode::Light)); + theme_mode_combo_->addItem("Custom", static_cast(ifcinterface::InterfaceSettings::ThemeMode::Custom)); + theme_form->addRow("Preset", theme_mode_combo_); + + theme_custom_body_ = new QWidget(theme_body); + auto* custom_grid = new QGridLayout(theme_custom_body_); + custom_grid->setContentsMargins(0, 0, 0, 0); + custom_grid->setHorizontalSpacing(12); + custom_grid->setVerticalSpacing(8); + + int row = 0; + for (const auto& spec : ifcinterface::InterfaceSettings::themeColorSpecs()) { + auto* label = new QLabel(QString::fromUtf8(spec.label), theme_custom_body_); + auto* edit = new QLineEdit(theme_custom_body_); + edit->setPlaceholderText("#000000"); + auto* pick = new QPushButton("Pick", theme_custom_body_); + connect(pick, &QPushButton::clicked, this, [this, edit]() { pickThemeColor(edit); }); + custom_grid->addWidget(label, row, 0); + custom_grid->addWidget(edit, row, 1); + custom_grid->addWidget(pick, row, 2); + theme_color_editors_.push_back({QString::fromUtf8(spec.key), edit}); + ++row; + } + + auto* theme_form_widget = new QWidget(theme_body); + theme_form_widget->setLayout(theme_form); + theme_layout->addWidget(theme_form_widget); + theme_layout->addWidget(theme_custom_body_); + theme_section->addBodyWidget(theme_body); + layout->addWidget(theme_section); + + connect(theme_mode_combo_, &QComboBox::currentIndexChanged, this, [this](int) { + updateThemeEditorEnabled(); + }); } - auto make_placeholder_tab = [tabs](const QString& title, const QString& detail) { - auto* tab = new QWidget(tabs); + auto make_placeholder_tab = [this](const QString& title, const QString& detail) { + auto* tab = new QWidget(this); auto* layout = new QVBoxLayout(tab); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(components::style::metrics::padding); + layout->setAlignment(Qt::AlignTop); auto* section = new components::Section(title, components::SectionHeaderMode::Visible, tab); auto* body = new QWidget(section); @@ -215,14 +265,10 @@ void SettingsDialog::setupUi() { return tab; }; - tabs->addTab(navigation_tab, "Navigation"); - tabs->addTab(make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here."), - "Keybindings"); - tabs->addTab(graphics_tab, "Graphics"); - tabs->addTab(make_placeholder_tab("Theme", "Theme, density, and UI appearance settings will live here."), - "Theme"); - tabs->addTab(make_placeholder_tab("About", "Version, credits, and environment information will live here."), - "About"); + addTab("Interface", interface_tab); + addTab("Keybindings", make_placeholder_tab("Keybindings", "Shortcut presets and command bindings will live here.")); + addTab("Graphics", graphics_tab); + addTab("About", make_placeholder_tab("About", "Version, credits, and environment information will live here.")); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); if (auto* ok = buttons->button(QDialogButtonBox::Ok)) { @@ -236,7 +282,6 @@ void SettingsDialog::setupUi() { connect(buttons, &QDialogButtonBox::accepted, this, &SettingsDialog::onAccepted); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); - addBodyWidget(tabs); addFooterWidget(buttons); } @@ -253,6 +298,37 @@ void SettingsDialog::syncFromSettings() { hiz_enabled_check_->setChecked(AppSettings::instance().hizEnabled()); hiz_resolution_spin_->setValue(AppSettings::instance().hizResolution()); nav_preset_combo_->setCurrentIndex(static_cast(AppSettings::instance().navPreset())); + syncThemeSettings(); +} + +void SettingsDialog::syncThemeSettings() { + const auto& settings = ifcinterface::InterfaceSettings::instance(); + const int idx = theme_mode_combo_->findData(static_cast(settings.themeMode())); + theme_mode_combo_->setCurrentIndex(idx >= 0 ? idx : 0); + for (auto& editor : theme_color_editors_) { + editor.edit->setText(settings.customColor(editor.key)); + } + updateThemeEditorEnabled(); +} + +void SettingsDialog::updateThemeEditorEnabled() { + if (!theme_mode_combo_ || !theme_custom_body_) return; + const auto mode = + static_cast(theme_mode_combo_->currentData().toInt()); + const bool is_custom = mode == ifcinterface::InterfaceSettings::ThemeMode::Custom; + theme_custom_body_->setVisible(is_custom); + theme_custom_body_->setEnabled(is_custom); +} + +void SettingsDialog::pickThemeColor(QLineEdit* edit) { + QColorDialog dialog(QColor(edit->text()), this); + dialog.setObjectName("appDialog"); + dialog.setWindowTitle("Choose Color"); + dialog.setOption(QColorDialog::DontUseNativeDialog, true); + if (dialog.exec() != QDialog::Accepted) return; + const QColor color = dialog.selectedColor(); + if (!color.isValid()) return; + edit->setText(color.name(QColor::HexRgb)); } void SettingsDialog::onAccepted() { @@ -269,6 +345,12 @@ void SettingsDialog::onAccepted() { AppSettings::instance().setHizResolution(hiz_resolution_spin_->value()); AppSettings::instance().setNavPreset( static_cast(nav_preset_combo_->currentIndex())); + auto& interface_settings = ifcinterface::InterfaceSettings::instance(); + interface_settings.setThemeMode( + static_cast(theme_mode_combo_->currentData().toInt())); + for (const auto& editor : theme_color_editors_) { + interface_settings.setCustomColor(editor.key, editor.edit->text()); + } accept(); } diff --git a/src/interface/modules/settings/Dialog.h b/src/interface/modules/settings/Dialog.h index 5aed5efe43..d021855884 100644 --- a/src/interface/modules/settings/Dialog.h +++ b/src/interface/modules/settings/Dialog.h @@ -23,16 +23,20 @@ #include "../../components/Dialog.h" +#include +#include + class QCheckBox; class QComboBox; class QDoubleSpinBox; class QLineEdit; class QShowEvent; class QSpinBox; +class QWidget; namespace ifcinterface::modules::settings { -class SettingsDialog : public components::Dialog { +class SettingsDialog : public components::TabbedDialog { Q_OBJECT public: explicit SettingsDialog(QWidget* parent = nullptr); @@ -43,8 +47,16 @@ protected: private: void setupUi(); void syncFromSettings(); + void syncThemeSettings(); + void updateThemeEditorEnabled(); + void pickThemeColor(QLineEdit* edit); void onAccepted(); + struct ThemeColorEditor { + QString key; + QLineEdit* edit = nullptr; + }; + QLineEdit* geometry_library_edit_ = nullptr; QCheckBox* show_stats_check_ = nullptr; QCheckBox* backface_culling_check_ = nullptr; @@ -57,6 +69,9 @@ private: QCheckBox* hiz_enabled_check_ = nullptr; QSpinBox* hiz_resolution_spin_ = nullptr; QComboBox* nav_preset_combo_ = nullptr; + QComboBox* theme_mode_combo_ = nullptr; + QWidget* theme_custom_body_ = nullptr; + std::vector theme_color_editors_; }; } // namespace ifcinterface::modules::settings diff --git a/src/interface/modules/viewport/Controller.cpp b/src/interface/modules/viewport/Controller.cpp index cc13890cf4..84153949f9 100644 --- a/src/interface/modules/viewport/Controller.cpp +++ b/src/interface/modules/viewport/Controller.cpp @@ -20,6 +20,7 @@ #include "Controller.h" +#include "../../InterfaceSettings.h" #include "../../SessionState.h" #include "../../../ifcviewer/AppSettings.h" #include "../../../ifcviewer/Federation.h" @@ -39,6 +40,12 @@ ViewportController::ViewportController(ifcinterface::SessionState* session_state { Federation* federation = session_state_->federation(); SceneLoader* loader = session_state_->loader(); + connect(&ifcinterface::InterfaceSettings::instance(), + &ifcinterface::InterfaceSettings::themeChanged, + this, [this]() { + viewport_->setBackgroundColor(QColor(ifcinterface::InterfaceSettings::instance().color("viewport_background"))); + }); + viewport_->setBackgroundColor(QColor(ifcinterface::InterfaceSettings::instance().color("viewport_background"))); connect(federation, &Federation::federatedFalseOriginChanged, this, &ViewportController::applyFederatedFalseOrigin); connect(federation, &Federation::configChanged, this, [this]() {