Skip to content

Add dark mode (System / Light / Dark theme setting)#1040

Open
aniro wants to merge 6 commits intodail8859:masterfrom
aniro:dark-mode-rework
Open

Add dark mode (System / Light / Dark theme setting)#1040
aniro wants to merge 6 commits intodail8859:masterfrom
aniro:dark-mode-rework

Conversation

@aniro
Copy link
Copy Markdown

@aniro aniro commented May 4, 2026

⚠️ AI-generated pull request.

Every commit, every line of code, the design choices, the commit messages, this PR description, and the test plan in this PR were all written by an LLM. A human reviewed and accepted the changes interactively but did not author them. Please review accordingly: the code compiles, runs, and was visually verified end-to-end on Linux (Wayland + GNOME, Qt 6.10), but a human maintainer's judgment on architecture and integration is still warranted before merge.

See `gist of changes` and `Test plan` below for review hooks.

Summary

Adds dark mode to NotepadNext via a tristate System / Light / Dark theme setting (default System). When set to System, the application follows the desktop environment's color scheme via `QStyleHints::colorScheme()` and updates live when the system preference changes. Light and Dark are explicit overrides for users who want their editor independent of the rest of the desktop.

This is an alternative take on #411 — written from scratch against current master rather than the older qmake codebase that PR was built on, and avoiding the third-party stylesheet dependency. #411 can be closed if this one is preferred.

Design

A single `ApplicationSettings::theme` enum drives everything. `effectiveDarkMode()` resolves System against the current `QStyleHints::colorScheme()`. A single `effectiveDarkModeChanged(bool)` signal fires on either user-initiated theme changes or system color-scheme changes (when in System mode), and every consumer connects to that one signal.

Theming layers:

  1. Qt widgets. `MainWindow::applyStyleSheet()` switches between Qt's natural palette (System mode — left untouched, since Qt 6.5+ already follows the desktop) and explicit Fusion-derived palettes (Light / Dark mode). Targeted CSS in `stylesheets/npp.css` (existing, light) and the new `stylesheets/npp-dark.css` (dark) covers ADS dock tabs, status bar, and `QuickFindWidget`. No third-party stylesheet dependency.

  2. Scintilla editor surfaces. `EditorManager::applyEditorTheme()` sets fold markers, element colors (caret line, selection inactive, whitespace, fold line), fold margin colors, and `STYLE_DEFAULT`. A separate `applyEditorNamedStyles()` sets `STYLE_LINENUMBER`, `STYLE_BRACELIGHT`, `STYLE_BRACEBAD`, `STYLE_INDENTGUIDE`. The split is needed because Lua's `SetStyle` calls `StyleClearAll` on every language load, which would otherwise wipe these named styles.

  3. Syntax highlighting. `init.lua` builds a `theme` table from a `dark_mode` global injected by C++. `SetStyle()` writes `STYLE_DEFAULT` from the theme, calls `StyleClearAll`, then auto-translates canonical light-mode colors per language style: `style.fgColor == light_fg → theme.default_fg` and `style.bgColor == light_bg → theme.default_bg`. The vast majority of language `.lua` files specify black on white and get auto-converted; deliberate syntax colors (blue keywords, green comments, gray strings, orange numbers) pass through unchanged and remain readable on a dark background. No language definition files were edited.

  4. `LuaConsoleDock`. Now takes `ApplicationSettings*`, applies theme-aware Lua lexer styles, and reapplies them on `effectiveDarkModeChanged`.

  5. Preferences UI. New "Theme" combo box (Follow system / Light / Dark) in the GUI tab.

Files

Area Change
`ApplicationSettings.{h,cpp}` `Theme` enum setting + `effectiveDarkMode()` helper + `effectiveDarkModeChanged` signal; subscribes to `QStyleHints::colorSchemeChanged`
`EditorManager.{h,cpp}` `applyEditorTheme()` + `applyEditorNamedStyles()`; signal connection
`NotepadNextApplication.{h,cpp}` `dark_mode` Lua injection; `refreshEditorTheme()` slot; `applyEditorNamedStyles` after Lua language load
`scripts/init.lua` `UpdateTheme()`, `theme` table, `SetStyle()` rewrite with auto-translation
`dialogs/MainWindow.cpp` `applyStyleSheet()` rewritten for tristate; signal connection
`dialogs/PreferencesDialog.{ui,cpp}` Theme combo box + wiring
`docks/LuaConsoleDock.{h,cpp}` Theme-aware lexer styles, signal connection
`stylesheets/npp-dark.css` (new) Dark variants for ADS tabs and `QuickFindWidget`
`resources.qrc` Register `npp-dark.css`

Verification done

  • Clean build against Qt 6.10 with the master CMake setup.
  • System theme with GNOME `prefer-dark`: app starts dark; switching GNOME to light makes the app re-theme live.
  • Theme=Light explicit: editor white, line numbers gray-on-`#E4E4E4`, menu/toolbar/tabs all light.
  • Theme=Dark explicit: editor `#1E1E1E`, line numbers `#858585`-on-`#252526`, full Qt widget palette dark.
  • C/C++ file in dark mode: keywords blue, strings gray, comments green, numbers orange — all readable on dark. No edits to language definition files.
  • Persisted in `~/.config/NotepadNext/NotepadNext.ini` under `[Gui]/Theme` (0 = System, 1 = Light, 2 = Dark).
  • Setting can be hand-edited or set via Preferences; live toggle confirmed.

Things explicitly NOT addressed

  • ADS auto-hide drop indicator alignment is broken in upstream master (and v0.13) on Qt 6.10/Wayland regardless of this PR. That looked like a regression at first; verified against vanilla master that it isn't. Belongs in a separate report against ADS or a version bump.
  • Only the most common language definitions exercise the auto-translation path. Languages whose `.lua` defines deliberately non-white backgrounds for some styles will keep those backgrounds in dark mode (e.g. error highlight). That's intentional but may surprise users; happy to add a more thorough overlay-table approach if desired.
  • macOS and Windows palette tuning. The hardcoded Light/Dark palettes are Fusion-derived neutral values that should look correct on all platforms, but only Linux has been visually tested.

Test plan

  • `Settings → Preferences → Theme` combo, switch between Follow system / Light / Dark; the editor and Qt chrome should update without restart.
  • With Theme=Follow system, toggle the desktop dark/light preference (GNOME Settings → Appearance, or `gsettings set org.gnome.desktop.interface color-scheme prefer-dark/default`); the app should re-theme live.
  • Open a few language files (C, Python, JSON, etc.) in both modes; syntax highlighting should remain readable.
  • Open the Lua console dock; theme should match.
  • Restart the app; the chosen theme should persist.
  • Sanity: build succeeds on Windows/macOS (haven't been able to verify; only `dialogs/MainWindow.cpp` adds platform-touchy code via `QPalette`/`QStyleHints`, both cross-platform).

aniro-s added 6 commits May 4, 2026 19:48
Implements a clean dark mode that drives all theming from a single ApplicationSettings::darkMode boolean. Live toggle: no restart required.

The Lua side defines a `theme` table that is rebuilt from a `dark_mode` global injected by C++. SetStyle() sets STYLE_DEFAULT from theme and calls StyleClearAll, then auto-translates canonical light-mode colors (black text, white background) to theme equivalents per-style. Language `.lua` files therefore work in either mode without modification: the vast majority specify black on white, which gets converted, while deliberate syntax colors (blue keywords, green comments, etc.) pass through unchanged.

EditorManager::applyEditorTheme() handles all Scintilla UI surfaces (margins, caret line, fold markers, line numbers, brace match) for both modes. NotepadNextApplication::refreshEditorTheme() updates the Lua theme and re-applies styles to every open editor when the setting changes.

For Qt widgets, MainWindow::applyStyleSheet() switches between Fusion-based light and dark QPalettes and loads either npp.css or the new npp-dark.css for ADS dock tabs and the QuickFindWidget. No third-party stylesheet dependency.

LuaConsoleDock takes ApplicationSettings and reapplies its lexer styles on darkModeChanged so the console's Lua syntax highlighting tracks the theme.

A "Dark mode" checkbox is added to the GUI section of Preferences.
Lua SetStyle() calls StyleClearAll on language load, which resets STYLE_LINENUMBER, STYLE_BRACELIGHT, STYLE_BRACEBAD, and STYLE_INDENTGUIDE to STYLE_DEFAULT values, wiping the colors set by applyEditorTheme.

Split applyEditorTheme into two methods: the full theme (including STYLE_DEFAULT + StyleClearAll) and a separate applyEditorNamedStyles for the colors that need re-applying after each language load. NotepadNextApplication::setEditorLanguage calls applyEditorNamedStyles after the Lua SetLanguage call so line numbers and brace match retain their themed colors regardless of which language is active.
Qt 6.5+ has Fusion::standardPalette() honor QStyleHints::colorScheme(), so on systems where the user has selected dark mode at the OS level (GNOME, KDE, etc.) Fusion::standardPalette() returns dark colors. Setting that as the application palette in light mode left menu bar, toolbar, and dock tabs dark while only the editor and stylesheet-targeted widgets switched to light.

Define an explicit light palette parallel to the existing dark one so the palette is fully driven by the user-selected DarkMode setting and ignores the system color scheme. Verified visually: menu bar, toolbar, and ADS tabs render light when DarkMode=false and dark when DarkMode=true, regardless of the GNOME theme.
Replace the DarkMode boolean with a Theme enum (System / Light / Dark, default System). System mode reads QGuiApplication::styleHints()->colorScheme() and listens for colorSchemeChanged so the app follows the desktop environment's light/dark preference, including live changes (e.g. GNOME night-mode auto-switching at sunset). Light and Dark remain explicit overrides for users who want their editor independent of the desktop theme.

ApplicationSettings exposes a single effectiveDarkMode() helper plus an effectiveDarkModeChanged(bool) signal that fires on either theme change or system color-scheme change while in System mode. All consumers (EditorManager, NotepadNextApplication, MainWindow, LuaConsoleDock) call effectiveDarkMode() and connect to that signal, so the resolution-to-dark/light logic stays in one place.

Preferences gets a "Theme" combo box with three options replacing the previous checkbox.
In System theme mode, applyStyleSheet now applies QApplication::style()->standardPalette() instead of our hardcoded Fusion palette. Qt 6.5+ Fusion already adapts standardPalette to QStyleHints::colorScheme(), so this lets the Qt widgets follow the desktop environment's palette rather than imposing our own interpretation of "dark" or "light".

Light and Dark explicit modes still use the hardcoded palettes since the user has chosen to override the system, and Qt's standardPalette would leak through the system color scheme there.

Scintilla editor surfaces still resolve through effectiveDarkMode() since they cannot read the Qt palette directly.
Stop forcing QApplication::setStyle(Fusion) globally and skip QApplication::setPalette() entirely when the user has selected System theme. Qt 6.5+ already syncs the palette with QStyleHints::colorScheme() and the platform integration picks the right style on its own; re-applying both on every applyStyleSheet call only re-emits paletteChange events and overrides legitimate platform choices.

Light and Dark explicit modes still apply their hardcoded Fusion-derived palettes since the user has chosen to override the system there.
@aniro aniro mentioned this pull request May 4, 2026
@aniro
Copy link
Copy Markdown
Author

aniro commented May 4, 2026

screenshots:
Screenshot From 2026-05-05 01-49-12
Screenshot From 2026-05-05 01-49-21
Screenshot From 2026-05-05 01-49-25

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants