How to Customize the Theme System with Qt Stylesheets in Fincept

Fincept's theme system uses a token-based architecture where ThemeManager generates global QSS from ThemeTokens and applies it via qApp->setStyleSheet(), allowing runtime customization without recompiling UI code.

The FinceptTerminal repository provides a sophisticated theming engine built on Qt's stylesheet (QSS) system that enables complete UI customization through centralized tokens. By leveraging the singleton ThemeManager and token-based color helpers defined in fincept-qt/src/ui/theme/, developers can modify application appearance, add new color palettes, or create entirely new themes while keeping widget code clean and maintainable.

Understanding Fincept's Theme Architecture

Fincept implements a centralized token system that separates visual values from UI implementation. The architecture follows a strict pipeline:

  1. ThemeTokens.h defines the ThemeTokens struct containing every visual property (colors, fonts, spacing) as plain data members like const char* bg_base or int font_size_base.
  2. ThemeManager (singleton) stores the active token set in current_ and generates the global QSS string through build_global_qss().
  3. Token helpers in Theme.h provide ColorToken and fonts objects that resolve live values lazily via operator const char*, ensuring widgets always read current theme data.
  4. Global application occurs through qApp->setStyleSheet(), followed by emission of the theme_changed(const ThemeTokens&) signal to notify custom-painted widgets.

The Advanced Docking System (ADS) receives separate styling through ThemeManager::build_ads_qss() (lines 86-104 in ThemeManager.cpp), ensuring dock widgets match the application theme.

Adding Custom Color Tokens

To introduce a new purple accent color for custom UI elements:

First, extend the token structure in fincept-qt/src/ui/theme/ThemeTokens.h:

struct ThemeTokens {
    // ... existing tokens ...
    const char* purple;  // new token
};

Next, populate the value in fincept-qt/src/ui/theme/ThemeManager.cpp:

const ThemeTokens THEME_OBSIDIAN = {
    // ... existing colors ...
    .purple = "#7e22ce",
};

Finally, expose the token via fincept-qt/src/ui/theme/Theme.h:

namespace colors {
    // ... existing tokens ...
    inline constexpr ColorToken PURPLE{&ThemeTokens::purple};
}

You can now use the token in any stylesheet builder:

QString customButtonStyle() {
    return QString(
        "QPushButton { background: %1; color: %2; border: 1px solid %3; }"
        "QPushButton:hover { background: %4; }"
    )
    .arg(colors::PURPLE(), colors::WHITE(), colors::BORDER_DIM(), colors::PURPLE());
}

Creating Widget-Specific Stylesheet Overrides

For targeted style changes without modifying the global theme, concatenate custom rules to the existing application stylesheet. This approach preserves the global theme while overriding specific widgets:

// In your widget implementation (e.g., NewsScreen.cpp)
QString newsCommandBarOverride = QString(
    "#newsCommandBar { background: %1; color: %2; border-bottom: 1px solid %3; }"
).arg(colors::PURPLE(), colors::WHITE(), colors::BORDER_BRIGHT());

// Append to existing global stylesheet
qApp->setStyleSheet(qApp->styleSheet() + newsCommandBarOverride);

Because Qt evaluates stylesheets in order, later rules take precedence. This technique allows module-specific styling while maintaining the token system for color values.

Implementing New Theme Presets

Fincept currently ships with the Obsidian theme. To add a light theme (e.g., "Alabaster"), define a new constant in ThemeManager.cpp:

const ThemeTokens THEME_ALABASTER = {
    .name = "Alabaster",
    .bg_base = "#ffffff",
    .bg_surface = "#f5f5f5",
    .bg_raised = "#e0e0e0",
    .bg_hover = "#dddddd",
    .border_dim = "#c0c0c0",
    .border_med = "#a0a0a0",
    .border_bright = "#808080",
    .text_primary = "#000000",
    .text_secondary = "#333333",
    .text_tertiary = "#666666",
    .text_dim = "#999999",
    .accent = "#0066cc",
    .accent_dim = "#003366",
    .text_on_accent = "#ffffff",
    .icon_dim = "#444444",
    .icon_hover = "#111111",
    .positive = "#28a745",
    .positive_dim = "#1e7e34",
    .negative = "#dc3545",
    .negative_dim = "#a71d2a",
    .warning = "#ffc107",
    .info = "#17a2b8",
    .cyan = "#17a2b8",
    .accent_bg = "#e6f0ff",
    .positive_bg = "#e6ffea",
    .negative_bg = "#ffe6e6",
    .row_alt = "#fafafa",
    .font_family = "'Consolas','Courier New',monospace",
    .font_size_base = 14,
    .chart_colors = {"#0066cc", "#17a2b8", "#28a745", "#dc3545", "#ffc107", "#6c757d"},
};

Then extend the theme switching logic:

void ThemeManager::apply_theme(const QString& name) {
    if (name == "Alabaster") 
        current_ = THEME_ALABASTER;
    else 
        current_ = THEME_OBSIDIAN;
    
    rebuild_and_apply();  // Regenerates QSS and applies to qApp
}

Users can now switch themes at runtime:

ThemeManager::instance().apply_theme("Alabaster");

Adjusting Content Density

Fincept supports Compact, Default, and Comfortable density modes that affect padding tokens. Modify density via:

ThemeManager::instance().apply_density("Compact");  // 2px padding

The density setting updates spacing tokens (%7 and %8 placeholders in build_global_qss) without requiring code changes in individual widgets. For one-off padding adjustments in custom components, use hardcoded values in your local QSS snippets rather than the global density tokens.

Handling Theme Changes in Custom-Painted Widgets

Widgets that perform custom painting must connect to the theme change signal to trigger repaints when colors update:

// MyChartWidget.cpp
MyChartWidget::MyChartWidget(QWidget* parent) : QWidget(parent) {
    connect(&fincept::ui::ThemeManager::instance(),
            &fincept::ui::ThemeManager::theme_changed,
            this, [this](const fincept::ui::ThemeTokens&) { 
                update();  // Trigger repaint
            });
}

void MyChartWidget::paintEvent(QPaintEvent*) {
    QPainter p(this);
    
    // Query live tokens directly
    p.fillRect(rect(), colors::BG_SURFACE());
    p.setPen(colors::POSITIVE());
    
    // Draw data series...
}

This pattern ensures custom graphics stay synchronized with the active theme, as implemented in fincept-qt/src/ui/widgets/StatusBar.cpp.

Summary

  • Token pipeline: Visual values flow from ThemeTokensThemeManager → global QSS string → qApp->setStyleSheet().
  • Key files: Define tokens in ThemeTokens.h, expose via Theme.h, and build QSS in ThemeManager.cpp or StyleSheets.cpp.
  • Runtime switching: Call ThemeManager::instance().apply_theme() to switch presets without restarting.
  • Widget integration: Connect to theme_changed signals for custom-painted widgets, or concatenate specific QSS rules for one-off overrides.
  • ADS support: Docking system styling is handled separately via build_ads_qss() in ThemeManager.cpp (lines 86-104).

Frequently Asked Questions

Where are theme tokens defined in the Fincept source code?

Theme tokens are defined in fincept-qt/src/ui/theme/ThemeTokens.h as a plain struct with color, font, and spacing members. The active values are stored in ThemeManager and accessed through token helpers like colors::BG_BASE() defined in fincept-qt/src/ui/theme/Theme.h.

How do I apply a custom Qt stylesheet to a single widget without affecting the global theme?

Concatenate your custom QSS to the existing application stylesheet using qApp->setStyleSheet(qApp->styleSheet() + customQss), or call setStyleSheet() directly on the specific widget. Use the token helpers (e.g., colors::PURPLE()) to maintain consistency with the active theme.

Can I switch themes at runtime in FinceptTerminal?

Yes. Call ThemeManager::instance().apply_theme("ThemeName") to switch presets dynamically. The method updates the internal current_ token set, rebuilds the global QSS via rebuild_and_apply(), and emits theme_changed to notify all listening widgets to refresh their appearance.

How do custom-painted widgets update when the theme changes?

Connect to the ThemeManager::theme_changed(const ThemeTokens&) signal in your widget constructor. When emitted, call update() to trigger a repaint, then query current colors through ThemeManager::instance().tokens() or the colors:: helper functions in your paintEvent handler.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →