diff --git a/CMakeLists.txt b/CMakeLists.txt index 857c8e3..851000f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,7 +139,9 @@ else() list(APPEND HEADERS src/core/caps_lock_voice_hotkey.h src/core/wayland_text_injector.h) add_compile_definitions(PLATFORM_LINUX) endif() -add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) +add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS} + src/ui/resources/styles/styles.qrc +) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src @@ -197,15 +199,6 @@ if(WIN32) endif() endif() -# ============================================================================ -# 资源文件 -# ============================================================================ -# 样式表 -qt_add_resources(${PROJECT_NAME} "styles" - PREFIX "/" - FILES - src/ui/resources/styles/main.qss -) # ============================================================================ # 安装 diff --git a/src/app/application.cpp b/src/app/application.cpp index be66b07..e9c4acb 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -3,11 +3,21 @@ #include "core/sense_voice_engine.h" #include "utils/logger.h" #include - -static const char* const kTag = "Application"; +#include +#include +#include +#include +#include +#include +#include +#include namespace impress { +static QString s_currentTheme; // 跟踪当前主题 + +static const char* const kTag = "Application"; + Application::Application(int& argc, char** argv) : QApplication(argc, argv) { @@ -71,4 +81,88 @@ void Application::loadGlobalModel() { sttEngine_->loadModelAsync(modelPath, tokensPath, device, numThreads); } +void Application::applyTheme(const QString& theme) { + s_currentTheme = theme; + + // 1. 先设置风格(必须在 palette 和 stylesheet 之前) + qApp->setStyle(QStyleFactory::create("Fusion")); + + // 2. 设置调色板 + QPalette palette; + if (theme == "dark") { + palette.setColor(QPalette::Window, QColor(53, 53, 53)); + palette.setColor(QPalette::WindowText, Qt::white); + palette.setColor(QPalette::Base, QColor(25, 25, 25)); + palette.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); + palette.setColor(QPalette::ToolTipBase, Qt::white); + palette.setColor(QPalette::ToolTipText, Qt::white); + palette.setColor(QPalette::Text, Qt::white); + palette.setColor(QPalette::Button, QColor(53, 53, 53)); + palette.setColor(QPalette::ButtonText, Qt::white); + palette.setColor(QPalette::BrightText, Qt::cyan); + palette.setColor(QPalette::Link, QColor(42, 130, 218)); + palette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + palette.setColor(QPalette::HighlightedText, Qt::black); + palette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); + } else { + palette = qApp->style()->standardPalette(); + } + qApp->setPalette(palette); + + // 3. 最后设置样式表(覆盖 palette) + const QString qssPath = (theme == "dark") ? ":/styles/main_dark.qss" : ":/styles/main.qss"; + QFile styleFile(qssPath); + if (styleFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qApp->setStyleSheet(styleFile.readAll()); + styleFile.close(); + } else { + LOG_ERROR("Theme", QString("无法加载样式表: %1").arg(qssPath)); + } + + LOG_INFO("Theme", QString("主题已切换: %1").arg(theme)); +} + +void Application::applyFontSize(int size) { + QFont font = qApp->font(); + font.setPointSize(size); + qApp->setFont(font); + LOG_INFO("Theme", QString("字体大小已设置: %1").arg(size)); +} + +QIcon Application::createTrayIcon(bool active) { + const QColor color = (s_currentTheme == "dark") ? Qt::white : Qt::black; + const int size = 16; + + QPixmap pixmap(size, size); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + + if (active) { + // 播放图标(三角形) + const int margin = 3; + QPolygon triangle; + triangle << QPoint(margin, margin) + << QPoint(margin, size - margin) + << QPoint(size - margin, size / 2); + painter.drawPolygon(triangle); + } else { + // 停止图标(正方形) + const int margin = 3; + painter.drawRect(margin, margin, size - 2 * margin, size - 2 * margin); + } + + return QIcon(pixmap); +} + +bool Application::isDarkTheme() { + return s_currentTheme == "dark"; +} + } // namespace impress diff --git a/src/app/application.h b/src/app/application.h index b30f48e..cda9f44 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -35,6 +35,18 @@ public: /** @brief 加载全局模型(在配置加载后手动调用) */ void loadGlobalModel(); + /** @brief 应用主题(light/dark),可在运行时调用 */ + static void applyTheme(const QString& theme); + + /** @brief 应用全局字体大小 */ + static void applyFontSize(int size); + + /** @brief 生成托盘图标(light=黑色,dark=白色) */ + static QIcon createTrayIcon(bool active); + + /** @brief 当前主题是否为 dark */ + static bool isDarkTheme(); + signals: /** @brief 模型加载中(带路径) */ void modelLoading(const QString& modelPath); diff --git a/src/main.cpp b/src/main.cpp index 2f206b0..55f108d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include int main(int argc, char* argv[]) { @@ -66,6 +67,13 @@ int main(int argc, char* argv[]) configManager->set("stt.model_path", modelPath); } + // 应用主题和字体 + QString theme = configManager->get("ui.theme").toString(); + int fontSize = configManager->get("ui.font_size").toInt(); + impress::Application::applyTheme(theme); + if (fontSize > 0) impress::Application::applyFontSize(fontSize); + LOG_INFO("Main", QString("主题: %1, 字体: %2").arg(theme).arg(fontSize)); + // 配置加载完成后,启动全局模型加载 app.loadGlobalModel(); diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index ae5fd63..c0ed8fb 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -5,6 +5,7 @@ #include "core/voice_input_service.h" #include "core/sense_voice_engine.h" #include "app/config_manager.h" +#include "app/application.h" #include "utils/logger.h" #include @@ -37,7 +38,6 @@ MainWindow::MainWindow(ConfigManager* configManager, setupMenuBar(); setupStatusBar(sttEngine); setupTrayIcon(); - loadStyleSheet(); // 初始化语音输入服务(共享全局引擎) voiceInputService_ = new VoiceInputService(configManager_, sttEngine, this); @@ -126,9 +126,9 @@ void MainWindow::setupTrayIcon() { trayIcon_ = new QSystemTrayIcon(this); trayIcon_->setContextMenu(trayMenu_); - // 默认状态:停止图标(SP_MediaStop) - idleIcon_ = style()->standardIcon(QStyle::SP_MediaStop); - activeIcon_ = style()->standardIcon(QStyle::SP_MediaPlay); + // 默认状态:根据主题颜色生成图标 + idleIcon_ = Application::createTrayIcon(false); + activeIcon_ = Application::createTrayIcon(true); trayIcon_->setIcon(idleIcon_); trayIcon_->setToolTip("Impress Voice Input - 语音输入就绪"); @@ -236,6 +236,17 @@ void MainWindow::updateModelStatus() { void MainWindow::onVoiceInputConfigChanged() { if (!voiceInputService_) return; + // 动态应用主题和字体 + QString theme = configManager_->get("ui.theme").toString(); + int fontSize = configManager_->get("ui.font_size").toInt(); + Application::applyTheme(theme); + if (fontSize > 0) Application::applyFontSize(fontSize); + + // 刷新托盘图标颜色 + idleIcon_ = Application::createTrayIcon(false); + activeIcon_ = Application::createTrayIcon(true); + updateTrayIcon("语音输入就绪"); + // 更新模型状态显示 updateModelStatus(); diff --git a/src/ui/resources/styles/main.qss b/src/ui/resources/styles/main.qss index fc51575..610af23 100644 --- a/src/ui/resources/styles/main.qss +++ b/src/ui/resources/styles/main.qss @@ -1,143 +1,157 @@ -/* Impress Voice Input - 全局样式表 */ +/* Impress Voice Input - 亮色主题样式表 */ /* ========== 全局 ========== */ * { font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; } -QMainWindow, QWidget { +QWidget { + background-color: #ffffff; + color: #1a1a1a; +} + +/* 容器控件必须显式设置背景 */ +QFrame { + background-color: #ffffff; +} + +QScrollArea, QScrollArea > QWidget { background-color: #ffffff; - color: #2c3e50; } /* ========== QTabWidget ========== */ QTabWidget::pane { - border: 1px solid #dcdfe6; - border-radius: 4px; - background: #fafafa; + border: 1px solid #e0e0e0; + border-radius: 6px; + background: #ffffff; } QTabBar::tab { - background: #f0f2f5; - border: 1px solid #dcdfe6; + background: #f5f5f5; + border: 1px solid #e0e0e0; border-bottom: none; - border-top-left-radius: 4px; - border-top-right-radius: 4px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; padding: 10px 24px; margin-right: 2px; font-size: 14px; - color: #606266; + color: #666666; } QTabBar::tab:selected { background: #ffffff; - border-bottom: 2px solid #409eff; - color: #409eff; + border-bottom: 2px solid #1976d2; + color: #1976d2; font-weight: bold; } QTabBar::tab:hover { - color: #409eff; + color: #1976d2; + background: #e8eaf6; } /* ========== QPushButton ========== */ QPushButton { background-color: #ffffff; - border: 1px solid #dcdfe6; + border: 1px solid #d0d0d0; border-radius: 4px; padding: 6px 16px; - color: #606266; + color: #333333; font-size: 13px; } QPushButton:hover { - background-color: #ecf5ff; - border-color: #b3d8ff; - color: #409eff; + background-color: #e3f2fd; + border-color: #1976d2; + color: #1976d2; } QPushButton:pressed { - background-color: #e6f0ff; + background-color: #bbdefb; } /* 主要操作按钮 */ QPushButton[objectName="saveBtn"], QPushButton[text="保存配置"] { - background-color: #409eff; + background-color: #1976d2; color: #ffffff; - border: 1px solid #409eff; + border: 1px solid #1976d2; } QPushButton[objectName="saveBtn"]:hover, QPushButton[text="保存配置"]:hover { - background-color: #66b1ff; + background-color: #1565c0; } /* 危险操作按钮 */ QPushButton[text="停止"], QPushButton[text="停止录音"] { - background-color: #f56c6c; + background-color: #e53935; color: #ffffff; - border: 1px solid #f56c6c; + border: 1px solid #e53935; } QPushButton[text="停止"]:hover, QPushButton[text="停止录音"]:hover { - background-color: #f78989; + background-color: #c62828; } /* ========== QGroupBox ========== */ QGroupBox { font-weight: bold; - border: 1px solid #dcdfe6; + border: 1px solid #e0e0e0; border-radius: 6px; margin-top: 12px; padding-top: 16px; + color: #1a1a1a; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 8px; - color: #303133; + color: #1a1a1a; } /* ========== QLabel ========== */ QLabel { - color: #606266; + color: #333333; + background: none; } /* ========== QLineEdit ========== */ QLineEdit { - border: 1px solid #dcdfe6; + border: 1px solid #d0d0d0; border-radius: 4px; padding: 5px 10px; background: #ffffff; + color: #1a1a1a; } QLineEdit:focus { - border-color: #409eff; + border-color: #1976d2; } QLineEdit[readOnly="true"] { - background-color: #f5f7fa; + background-color: #f5f5f5; } /* ========== QComboBox ========== */ QComboBox { - border: 1px solid #dcdfe6; + border: 1px solid #d0d0d0; border-radius: 4px; padding: 5px 10px; background: #ffffff; + color: #1a1a1a; min-width: 120px; } QComboBox:hover { - border-color: #c0c4cc; + border-color: #909090; } QComboBox:focus { - border-color: #409eff; + border-color: #1976d2; } QComboBox::drop-down { @@ -145,52 +159,62 @@ QComboBox::drop-down { width: 24px; } +QComboBox QAbstractItemView { + background-color: #ffffff; + color: #1a1a1a; + selection-background-color: #e3f2fd; +} + /* ========== QSpinBox / QDoubleSpinBox ========== */ QSpinBox, QDoubleSpinBox { - border: 1px solid #dcdfe6; + border: 1px solid #d0d0d0; border-radius: 4px; padding: 4px 8px; background: #ffffff; + color: #1a1a1a; } QSpinBox:focus, QDoubleSpinBox:focus { - border-color: #409eff; + border-color: #1976d2; } /* ========== QProgressBar ========== */ QProgressBar { - border: 1px solid #dcdfe6; + border: 1px solid #e0e0e0; border-radius: 4px; text-align: center; height: 20px; - background: #f5f7fa; + background: #f5f5f5; + color: #333333; } QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, - stop:0 #409eff, stop:1 #66b1ff); + stop:0 #1976d2, stop:1 #42a5f5); border-radius: 3px; } /* ========== QTextEdit ========== */ QTextEdit { - border: 1px solid #dcdfe6; + border: 1px solid #d0d0d0; border-radius: 4px; padding: 8px; background: #ffffff; - selection-background-color: #ecf5ff; + color: #1a1a1a; + selection-background-color: #e3f2fd; } QTextEdit:focus { - border-color: #409eff; + border-color: #1976d2; } /* ========== QListWidget ========== */ QListWidget { - border: 1px solid #dcdfe6; + border: 1px solid #e0e0e0; border-radius: 4px; background: #ffffff; padding: 4px; + color: #1a1a1a; } QListWidget::item { @@ -199,58 +223,62 @@ QListWidget::item { } QListWidget::item:selected { - background-color: #ecf5ff; - color: #409eff; + background-color: #e3f2fd; + color: #1976d2; } QListWidget::item:hover { - background-color: #f5f7fa; + background-color: #f5f5f5; } /* ========== QCheckBox ========== */ QCheckBox { spacing: 8px; + color: #333333; } QCheckBox::indicator { width: 16px; height: 16px; - border: 1px solid #dcdfe6; + border: 1px solid #d0d0d0; border-radius: 3px; + background-color: #ffffff; } QCheckBox::indicator:checked { - background-color: #409eff; - border-color: #409eff; + background-color: #1976d2; + border-color: #1976d2; } /* ========== QMenu / QMenuBar ========== */ QMenuBar { background-color: #ffffff; - border-bottom: 1px solid #dcdfe6; + border-bottom: 1px solid #e0e0e0; padding: 2px; + color: #333333; } QMenuBar::item:selected { - background-color: #ecf5ff; - color: #409eff; + background-color: #e3f2fd; + color: #1976d2; } QMenu { background-color: #ffffff; - border: 1px solid #dcdfe6; + border: 1px solid #e0e0e0; border-radius: 4px; padding: 4px; + color: #333333; } QMenu::item:selected { - background-color: #ecf5ff; - color: #409eff; + background-color: #e3f2fd; + color: #1976d2; } QMenu::separator { height: 1px; - background-color: #ebeef5; + background-color: #e0e0e0; margin: 4px 0; } @@ -260,25 +288,25 @@ QMessageBox { } QMessageBox QLabel { - color: #303133; + color: #1a1a1a; } /* ========== 滚动条 ========== */ QScrollBar:vertical { border: none; - background: #f5f7fa; + background: #f5f5f5; width: 8px; border-radius: 4px; } QScrollBar::handle:vertical { - background: #c0c4cc; + background: #bdbdbd; border-radius: 4px; min-height: 30px; } QScrollBar::handle:vertical:hover { - background: #909399; + background: #9e9e9e; } QScrollBar::add-line:vertical, diff --git a/src/ui/resources/styles/main_dark.qss b/src/ui/resources/styles/main_dark.qss new file mode 100644 index 0000000..d633104 --- /dev/null +++ b/src/ui/resources/styles/main_dark.qss @@ -0,0 +1,312 @@ +/* Impress Voice Input - 暗色主题样式表 */ + +/* ========== 全局 ========== */ +* { + font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; +} + +QWidget { + background-color: #353535; + color: #ffffff; +} + +QFrame { + background-color: #353535; +} + +QScrollArea, QScrollArea > QWidget { + background-color: #2a2a2a; +} + +/* ========== QTabWidget ========== */ +QTabWidget::pane { + border: 1px solid #555555; + border-radius: 4px; + background: #2a2a2a; +} + +QTabBar::tab { + background: #3a3a3a; + border: 1px solid #555555; + border-bottom: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 10px 24px; + margin-right: 2px; + font-size: 14px; + color: #cccccc; +} + +QTabBar::tab:selected { + background: #2a2a2a; + border-bottom: 2px solid #4286f4; + color: #4286f4; + font-weight: bold; +} + +QTabBar::tab:hover { + color: #4286f4; +} + +/* ========== QPushButton ========== */ +QPushButton { + background-color: #353535; + border: 1px solid #555555; + border-radius: 4px; + padding: 6px 16px; + color: #cccccc; + font-size: 13px; +} + +QPushButton:hover { + background-color: #4286f4; + border-color: #4286f4; + color: #ffffff; +} + +QPushButton:pressed { + background-color: #3a6fd8; +} + +/* 主要操作按钮 */ +QPushButton[objectName="saveBtn"], +QPushButton[text="保存配置"] { + background-color: #4286f4; + color: #ffffff; + border: 1px solid #4286f4; +} + +QPushButton[objectName="saveBtn"]:hover, +QPushButton[text="保存配置"]:hover { + background-color: #5a9aff; +} + +/* 危险操作按钮 */ +QPushButton[text="停止"], +QPushButton[text="停止录音"] { + background-color: #e74c3c; + color: #ffffff; + border: 1px solid #e74c3c; +} + +QPushButton[text="停止"]:hover, +QPushButton[text="停止录音"]:hover { + background-color: #f05e50; +} + +/* ========== QGroupBox ========== */ +QGroupBox { + font-weight: bold; + border: 1px solid #555555; + border-radius: 6px; + margin-top: 12px; + padding-top: 16px; + color: #ffffff; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top left; + padding: 0 8px; + color: #ffffff; +} + +/* ========== QLabel ========== */ +QLabel { + color: #cccccc; +} + +/* ========== QLineEdit ========== */ +QLineEdit { + border: 1px solid #555555; + border-radius: 4px; + padding: 5px 10px; + background: #1a1a1a; + color: #ffffff; +} + +QLineEdit:focus { + border-color: #4286f4; +} + +QLineEdit[readOnly="true"] { + background-color: #2a2a2a; +} + +/* ========== QComboBox ========== */ +QComboBox { + border: 1px solid #555555; + border-radius: 4px; + padding: 5px 10px; + background: #1a1a1a; + color: #ffffff; + min-width: 120px; +} + +QComboBox:hover { + border-color: #888888; +} + +QComboBox:focus { + border-color: #4286f4; +} + +QComboBox::drop-down { + border: none; + width: 24px; +} + +QComboBox QAbstractItemView { + background-color: #1a1a1a; + color: #ffffff; + selection-background-color: #4286f4; +} + +/* ========== QSpinBox / QDoubleSpinBox ========== */ +QSpinBox, QDoubleSpinBox { + border: 1px solid #555555; + border-radius: 4px; + padding: 4px 8px; + background: #1a1a1a; + color: #ffffff; +} + +QSpinBox:focus, QDoubleSpinBox:focus { + border-color: #4286f4; +} + +/* ========== QProgressBar ========== */ +QProgressBar { + border: 1px solid #555555; + border-radius: 4px; + text-align: center; + height: 20px; + background: #2a2a2a; + color: #ffffff; +} + +QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #4286f4, stop:1 #5a9aff); + border-radius: 3px; +} + +/* ========== QTextEdit ========== */ +QTextEdit { + border: 1px solid #555555; + border-radius: 4px; + padding: 8px; + background: #1a1a1a; + color: #ffffff; + selection-background-color: #4286f4; +} + +QTextEdit:focus { + border-color: #4286f4; +} + +/* ========== QListWidget ========== */ +QListWidget { + border: 1px solid #555555; + border-radius: 4px; + background: #1a1a1a; + padding: 4px; + color: #ffffff; +} + +QListWidget::item { + padding: 8px; + border-radius: 4px; +} + +QListWidget::item:selected { + background-color: #4286f4; + color: #ffffff; +} + +QListWidget::item:hover { + background-color: #3a3a3a; +} + +/* ========== QCheckBox ========== */ +QCheckBox { + spacing: 8px; + color: #cccccc; +} + +QCheckBox::indicator { + width: 16px; + height: 16px; + border: 1px solid #555555; + border-radius: 3px; + background-color: #1a1a1a; +} + +QCheckBox::indicator:checked { + background-color: #4286f4; + border-color: #4286f4; +} + +/* ========== QMenu / QMenuBar ========== */ +QMenuBar { + background-color: #353535; + border-bottom: 1px solid #555555; + padding: 2px; + color: #cccccc; +} + +QMenuBar::item:selected { + background-color: #4286f4; + color: #ffffff; +} + +QMenu { + background-color: #353535; + border: 1px solid #555555; + border-radius: 4px; + padding: 4px; + color: #cccccc; +} + +QMenu::item:selected { + background-color: #4286f4; + color: #ffffff; +} + +QMenu::separator { + height: 1px; + background-color: #555555; + margin: 4px 0; +} + +/* ========== QMessageBox ========== */ +QMessageBox { + background-color: #353535; +} + +QMessageBox QLabel { + color: #ffffff; +} + +/* ========== 滚动条 ========== */ +QScrollBar:vertical { + border: none; + background: #2a2a2a; + width: 8px; + border-radius: 4px; +} + +QScrollBar::handle:vertical { + background: #666666; + border-radius: 4px; + min-height: 30px; +} + +QScrollBar::handle:vertical:hover { + background: #888888; +} + +QScrollBar::add-line:vertical, +QScrollBar::sub-line:vertical { + height: 0; +} diff --git a/src/ui/resources/styles/styles.qrc b/src/ui/resources/styles/styles.qrc new file mode 100644 index 0000000..3523715 --- /dev/null +++ b/src/ui/resources/styles/styles.qrc @@ -0,0 +1,6 @@ + + + main.qss + main_dark.qss + +