diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 11fc9a0..d8193ea 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -25,7 +25,8 @@ add_executable(geopro_desktop WIN32 panels/DatasetListPanel.cpp panels/ObjectTreePanel.cpp CentralScene.cpp - ProjectListDialog.cpp) + ProjectListDialog.cpp + SettingsDialog.cpp) target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) # QtKeychain 经 FetchContent 接入,头文件不随 target 传播,显式加源/构建目录(含生成的 export 头)。 diff --git a/src/app/PanelHeader.cpp b/src/app/PanelHeader.cpp index 832555c..aadb7a7 100644 --- a/src/app/PanelHeader.cpp +++ b/src/app/PanelHeader.cpp @@ -46,12 +46,12 @@ QString headerQss() "QToolButton#tabBtn:hover { color:#1F2A3D; }" "QToolButton#tabBtn:checked { color:#2D6CB5; font-weight:%3;" " border-bottom:2px solid #2D6CB5; }") - .arg(type::kTitle) - .arg(type::kCaption) + .arg(scaledPx(type::kTitle)) + .arg(scaledPx(type::kCaption)) .arg(type::kWeightSemibold) .arg(QString::fromUtf8(semantic::kWarningFill)) .arg(QString::fromUtf8(semantic::kWarning)) - .arg(type::kBody); + .arg(scaledPx(type::kBody)); } // 数量徽标(默认隐藏,调用方 setText+setVisible 显示)。 diff --git a/src/app/SettingsDialog.cpp b/src/app/SettingsDialog.cpp new file mode 100644 index 0000000..5a695e6 --- /dev/null +++ b/src/app/SettingsDialog.cpp @@ -0,0 +1,156 @@ +#include "SettingsDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Theme.hpp" + +namespace geopro::app { + +namespace { + +// 「标签 + 控件」一行(标签定宽左对齐,控件右随)。 +QWidget* makeRow(const QString& label, QWidget* control) { + auto* row = new QWidget(); + auto* lay = new QHBoxLayout(row); + lay->setContentsMargins(0, 0, 0, 0); + lay->setSpacing(12); + auto* lbl = new QLabel(label, row); + lbl->setMinimumWidth(96); + lay->addWidget(lbl); + lay->addWidget(control, 1); + return row; +} + +// 区段标题。 +ElaText* sectionTitle(const QString& text, QWidget* parent) { + auto* t = new ElaText(text, parent); + t->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kHeading)); + return t; +} + +QWidget* buildAppearancePage() { + auto* page = new QWidget(); + auto* v = new QVBoxLayout(page); + v->setContentsMargins(24, 20, 24, 20); + v->setSpacing(16); + v->addWidget(sectionTitle(QStringLiteral("外观"), page)); + + // 主题:跟随系统 / 浅色 / 深色(热切)。 + auto* themeCombo = new ElaComboBox(page); + themeCombo->addItem(QStringLiteral("跟随系统"), QStringLiteral("system")); + themeCombo->addItem(QStringLiteral("浅色"), QStringLiteral("light")); + themeCombo->addItem(QStringLiteral("深色"), QStringLiteral("dark")); + const QString curTheme = geopro::app::themeModePreference(); + themeCombo->setCurrentIndex(themeCombo->findData(curTheme) >= 0 ? themeCombo->findData(curTheme) : 0); + QObject::connect(themeCombo, &QComboBox::activated, page, [themeCombo](int) { + geopro::app::setThemeModePreference(themeCombo->currentData().toString()); + }); + v->addWidget(makeRow(QStringLiteral("主题"), themeCombo)); + + // 界面字号:小/标准/大/特大(重启生效)。 + auto* fontCombo = new ElaComboBox(page); + fontCombo->addItem(QStringLiteral("小"), 90); + fontCombo->addItem(QStringLiteral("标准"), 100); + fontCombo->addItem(QStringLiteral("大"), 115); + fontCombo->addItem(QStringLiteral("特大"), 130); + const int curScale = geopro::app::fontScalePreference(); + fontCombo->setCurrentIndex(fontCombo->findData(curScale) >= 0 ? fontCombo->findData(curScale) : 1); + v->addWidget(makeRow(QStringLiteral("界面字号"), fontCombo)); + + // 字号改动:持久化 + 提示重启(提供立即重启)。 + auto* restartRow = new QWidget(page); + auto* rlay = new QHBoxLayout(restartRow); + rlay->setContentsMargins(96 + 12, 0, 0, 0); // 与控件列对齐 + rlay->setSpacing(10); + auto* hint = new ElaText(QStringLiteral("界面字号将在重启后生效"), restartRow); + hint->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kCaption)); + auto* restartBtn = new ElaPushButton(QStringLiteral("立即重启"), restartRow); + rlay->addWidget(hint); + rlay->addWidget(restartBtn); + rlay->addStretch(); + restartRow->setVisible(false); + v->addWidget(restartRow); + + QObject::connect(fontCombo, &QComboBox::activated, page, [fontCombo, restartRow](int) { + geopro::app::setFontScalePreference(fontCombo->currentData().toInt()); + restartRow->setVisible(true); + }); + QObject::connect(restartBtn, &QPushButton::clicked, restartBtn, [] { + QProcess::startDetached(QCoreApplication::applicationFilePath(), + QCoreApplication::arguments().mid(1)); + qApp->quit(); + }); + + v->addStretch(); + return page; +} + +QWidget* buildAboutPage() { + auto* page = new QWidget(); + auto* v = new QVBoxLayout(page); + v->setContentsMargins(24, 20, 24, 20); + v->setSpacing(12); + v->addWidget(sectionTitle(QStringLiteral("关于"), page)); + + auto* ver = new ElaText(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)"), page); + ver->setTextPixelSize(geopro::app::scaledPx(geopro::app::type::kTitle)); + v->addWidget(ver); + + auto* license = new QTextBrowser(page); + license->setOpenExternalLinks(true); + license->setHtml(QStringLiteral( + "第三方组件与许可证" + "" + "" + "" + "" + "" + "" + "
Qt 6GUI 框架LGPL-3.0
VTK 9二维/三维渲染BSD-3-Clause
Qt-Advanced-Docking-System停靠布局LGPL-2.1
ElaWidgetToolsFluent 控件MIT
QtKeychain凭证安全存取BSD-3-Clause
" + "

完整声明见随附 NOTICE.md

")); + v->addWidget(license, 1); + return page; +} + +} // namespace + +SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent) { + setWindowTitle(QStringLiteral("设置")); + resize(720, 480); + + auto* root = new QHBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // 左:分类列表。 + auto* sidebar = new QListWidget(this); + sidebar->setObjectName(QStringLiteral("settingsSidebar")); + sidebar->setFixedWidth(150); + sidebar->addItem(QStringLiteral("外观")); + sidebar->addItem(QStringLiteral("关于")); + root->addWidget(sidebar); + + // 右:分页。 + auto* stack = new QStackedWidget(this); + stack->addWidget(buildAppearancePage()); + stack->addWidget(buildAboutPage()); + root->addWidget(stack, 1); + + QObject::connect(sidebar, &QListWidget::currentRowChanged, stack, + &QStackedWidget::setCurrentIndex); + sidebar->setCurrentRow(0); +} + +} // namespace geopro::app diff --git a/src/app/SettingsDialog.hpp b/src/app/SettingsDialog.hpp new file mode 100644 index 0000000..282c18e --- /dev/null +++ b/src/app/SettingsDialog.hpp @@ -0,0 +1,15 @@ +#pragma once +#include + +namespace geopro::app { + +// 设置对话框(左分类 + 右内容页,参考常见客户端布局): +// 外观 —— 主题(跟随系统/浅色/深色,热切) + 界面字号(小/标准/大/特大,重启生效) +// 关于 —— 版本号 + 第三方组件与许可证 +class SettingsDialog : public QDialog { + Q_OBJECT +public: + explicit SettingsDialog(QWidget* parent = nullptr); +}; + +} // namespace geopro::app diff --git a/src/app/Theme.cpp b/src/app/Theme.cpp index c287dce..4f8471f 100644 --- a/src/app/Theme.cpp +++ b/src/app/Theme.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -473,6 +474,60 @@ void applyBrandAccent() eTheme->setThemeColor(ElaThemeType::Dark, ElaThemeType::BasicSelectedHoverAlpha, QColor(0x3C, 0x5D, 0x87)); } +// ── 设置:主题 / 字号 偏好 ────────────────────────────────────────────── +namespace { +constexpr int kBaseFontPx = 13; // 基准字号(与 eApp 默认一致) +int g_fontScale = 100; // 当前字号缩放百分比 +} // namespace + +QString themeModePreference() +{ + return QSettings().value(QStringLiteral("ui/themeMode"), QStringLiteral("system")).toString(); +} + +void applyPersistedThemeMode() +{ + const QString m = themeModePreference(); + if (m == QStringLiteral("light")) { + eTheme->setIsFollowSystemTheme(false); + eTheme->setThemeMode(ElaThemeType::Light); + } else if (m == QStringLiteral("dark")) { + eTheme->setIsFollowSystemTheme(false); + eTheme->setThemeMode(ElaThemeType::Dark); + } else { // system:跟随系统明暗 + eTheme->setIsFollowSystemTheme(true); + } +} + +void setThemeModePreference(const QString& mode) +{ + QSettings().setValue(QStringLiteral("ui/themeMode"), mode); + applyPersistedThemeMode(); // 主题可热切,立即应用 +} + +int fontScalePreference() +{ + return QSettings().value(QStringLiteral("ui/fontScale"), 100).toInt(); +} + +void setFontScalePreference(int percent) +{ + QSettings().setValue(QStringLiteral("ui/fontScale"), percent); // 重启后生效 +} + +void applyPersistedFontScale() +{ + g_fontScale = fontScalePreference(); + QFont f = qApp->font(); + f.setPixelSize(kBaseFontPx * g_fontScale / 100); + qApp->setFont(f); +} + +int scaledPx(int basePx) +{ + return basePx * g_fontScale / 100; +} + bool isDarkTheme() { return eTheme->getThemeMode() == ElaThemeType::Dark; diff --git a/src/app/Theme.hpp b/src/app/Theme.hpp index b80f69e..0563274 100644 --- a/src/app/Theme.hpp +++ b/src/app/Theme.hpp @@ -94,6 +94,19 @@ void applyTheme(QApplication& app); // (选中/激活/标题栏强调) 与本项目 QSS 共用同一套蓝,消除"多种蓝打架"。在 eApp->init() 后调一次。 void applyBrandAccent(); +// ── 设置:主题 / 界面字号 偏好(QSettings 持久化)──────────────────────── +// 启动时(eApp->init + applyBrandAccent 之后、弹登录窗之前)各调一次,使登录页与主页统一。 +void applyPersistedThemeMode(); // 应用持久化主题:跟随系统 / 浅色 / 深色 → ElaTheme +void applyPersistedFontScale(); // 应用持久化字号:设 qApp 基准字体 + 记录缩放(供 scaledPx) + +QString themeModePreference(); // "system" | "light" | "dark"(默认 system) +void setThemeModePreference(const QString& mode); // 持久化 + 立即应用(主题可热切) + +int fontScalePreference(); // 缩放百分比 90/100/115/130(默认 100) +void setFontScalePreference(int percent); // 仅持久化(字号改动重启后生效) + +int scaledPx(int basePx); // basePx × 当前字号% / 100(内联 QSS 字号用,使自定义 chrome 也随字号缩放) + // 当前 ElaTheme 是否暗色(供内联样式判断)。 bool isDarkTheme(); diff --git a/src/app/TopBar.cpp b/src/app/TopBar.cpp index 8272249..73295b3 100644 --- a/src/app/TopBar.cpp +++ b/src/app/TopBar.cpp @@ -1,5 +1,6 @@ #include "TopBar.hpp" +#include #include #include #include @@ -145,12 +146,12 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { " font-size:%1px; }" "#userName { color:#1F2A3D; font-size:%3px; font-weight:%4; }" "#userRole { color:#8A93A3; font-size:%5px; }") - .arg(type::kBody) + .arg(scaledPx(type::kBody)) .arg(type::kWeightBold) - .arg(type::kLabel) + .arg(scaledPx(type::kLabel)) .arg(type::kWeightSemibold) - .arg(type::kCaption) - .arg(type::kTitle)); + .arg(scaledPx(type::kCaption)) + .arg(scaledPx(type::kTitle))); auto* lay = new QHBoxLayout(this); lay->setContentsMargins(14, 0, 14, 0); @@ -188,7 +189,10 @@ TopBar::TopBar(QWidget* parent) : QWidget(parent) { lay->addWidget(makeIconButton(this, ElaIconType::CircleQuestion, QStringLiteral("帮助"))); lay->addWidget(makeIconButton(this, ElaIconType::Bell, QStringLiteral("通知"))); - lay->addWidget(makeIconButton(this, ElaIconType::Gear, QStringLiteral("设置"))); + auto* gearBtn = makeIconButton(this, ElaIconType::Gear, QStringLiteral("设置")); + if (auto* gb = qobject_cast(gearBtn)) + QObject::connect(gb, &QAbstractButton::clicked, this, [this] { emit settingsRequested(); }); + lay->addWidget(gearBtn); lay->addSpacing(10); lay->addWidget(makeDivider(this)); lay->addSpacing(12); diff --git a/src/app/TopBar.hpp b/src/app/TopBar.hpp index 662bed6..f90445e 100644 --- a/src/app/TopBar.hpp +++ b/src/app/TopBar.hpp @@ -26,6 +26,7 @@ signals: void projectSwitchRequested(const QString& projectId); void allProjectsRequested(); // 点击"全部项目…" void logoutRequested(); // 头像菜单「退出登录」 + void settingsRequested(); // 点击齿轮图标 → 打开设置 private: QToolButton* wsBtn_ = nullptr; diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp index fbd43ae..8df61fd 100644 --- a/src/app/login/LoginWindow.cpp +++ b/src/app/login/LoginWindow.cpp @@ -104,10 +104,10 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) "#fieldLabel { color: #5A6B85; font-size: %4px; font-weight: %5; }" // 输入框已 Ela 化(ElaLineEdit 自绘 Fluent + 自动明暗),不再写 QLineEdit QSS。 "#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }") - .arg(type::kDisplay) + .arg(scaledPx(type::kDisplay)) .arg(type::kWeightBold) - .arg(type::kCaption) - .arg(type::kCaption) + .arg(scaledPx(type::kCaption)) + .arg(scaledPx(type::kCaption)) .arg(type::kWeightSemibold)); auto* root = new QVBoxLayout(this); @@ -198,7 +198,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) // 错误提示:固定占位高度,避免出现时整体布局跳动。 errorLabel_ = new QLabel(body); geopro::app::applyThemedStyleSheet( - errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(type::kCaption)); + errorLabel_, QStringLiteral("color: #C0392B; font-size: %1px;").arg(scaledPx(type::kCaption))); errorLabel_->setWordWrap(true); errorLabel_->setMinimumHeight(18); form->addWidget(errorLabel_); diff --git a/src/app/main.cpp b/src/app/main.cpp index 6e6a92d..a493a93 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -81,6 +81,7 @@ #include "Glyphs.hpp" #include "PanelHeader.hpp" #include "Theme.hpp" +#include "SettingsDialog.hpp" #include "TopBar.hpp" #include "CentralScene.hpp" #include "ProjectListDialog.hpp" @@ -335,7 +336,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re "QToolButton:hover{ background:#EEF3FB; }" "QToolButton:checked{ color:#2D6CB5; font-weight:%2;" " border-bottom:2px solid #2D6CB5; }") - .arg(geopro::app::type::kBody) + .arg(geopro::app::scaledPx(geopro::app::type::kBody)) .arg(geopro::app::type::kWeightSemibold); // 工具条:「二维地图/三维视图」两个互斥可勾选按钮。默认二维地图。 @@ -380,7 +381,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re layerTitle, QStringLiteral("font-weight:%1;color:#2D6CB5;border:none;background:transparent;" "padding-bottom:3px;font-size:%2px;") .arg(geopro::app::type::kWeightSemibold) - .arg(geopro::app::type::kTitle)); + .arg(geopro::app::scaledPx(geopro::app::type::kTitle))); auto* chkCurtain = new ElaCheckBox(QStringLiteral("帘面(断面墙)")); chkCurtain->setChecked(true); auto* chkVoxel = new ElaCheckBox(QStringLiteral("体素(dd_voxel)")); @@ -431,7 +432,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re esTitle->setAlignment(Qt::AlignCenter); geopro::app::applyThemedStyleSheet( esTitle, QStringLiteral("color:#5A6B85; font-size:%1px; font-weight:%2;") - .arg(geopro::app::type::kHeading) + .arg(geopro::app::scaledPx(geopro::app::type::kHeading)) .arg(geopro::app::type::kWeightSemibold)); auto* esHint = new QLabel(QStringLiteral("单击左侧采集批次,查看反演剖面与异常点\n" @@ -439,7 +440,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re emptyState); esHint->setAlignment(Qt::AlignCenter); geopro::app::applyThemedStyleSheet( - esHint, QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::type::kBody)); + esHint, + QStringLiteral("color:#8A93A3; font-size:%1px;").arg(geopro::app::scaledPx(geopro::app::type::kBody))); esLay->addWidget(esIcon); esLay->addWidget(esTitle); @@ -930,6 +932,11 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re QCoreApplication::arguments().mid(1)); qApp->quit(); }); + // 设置:点齿轮 → 打开设置对话框(外观/关于)。 + QObject::connect(topBar, &geopro::app::TopBar::settingsRequested, &window, [&window]() { + geopro::app::SettingsDialog dlg(&window); + dlg.exec(); + }); QObject::connect(topBar, &geopro::app::TopBar::allProjectsRequested, &window, [&projectRepo, &nav, topBar, &window]() { auto* dlg = new geopro::app::ProjectListDialog(projectRepo, &window); @@ -1059,10 +1066,12 @@ int main(int argc, char* argv[]) // ElaApplication:Fluent 主题/字体/动画基建。无条件初始化——登录窗与各面板已 Ela 化, // 两种壳都需要它(登录发生在选壳之前)。Ela 控件跟随 ElaTheme;标准控件仍由下面 QSS 接管。 eApp->init(); - geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝 + geopro::app::applyBrandAccent(); // 统一品牌强调色(Ela Primary),全 UI 选中/激活一套蓝 + geopro::app::applyPersistedThemeMode(); // 应用持久化主题(跟随系统/浅/深)——登录页与主页统一 + geopro::app::applyPersistedFontScale(); // 应用持久化字号缩放(登录页也随之缩放) // 专业主题(Fusion + 调色板 + 全局样式表):标准控件外观,登录窗与工作台共用。 - // 跟随 ElaTheme 初始模式(可能随系统为暗),使登录窗与标准控件明暗一致(review M2)。 + // 跟随最终 ElaTheme 模式(持久化偏好已生效),使登录窗与标准控件明暗一致。 geopro::app::applyThemeMode(app, eTheme->getThemeMode() == ElaThemeType::Dark); // PROJ 数据(proj.db)定位:体素配准的 CrsTransform 需要。优先已设环境变量;