feat: dark mode support for pyRevit WPF forms - #3582
Conversation
Add a centralized Theme.xaml resource dictionary and inject a semantic light/dark palette into every WPF control's resources at construction, selected from Revit's active UITheme. Theme.xaml re-templates the standard WPF controls (Button, TextBox, ComboBox, ListBox/ListView, CheckBox, RadioButton, Expander, GroupBox, TabItem, ScrollBar, ContextMenu, MenuItem, ToolTip, Separator, Label) against pyRevit<*>Brush resources so descendants pick the active palette via DynamicResource. WPFWindow now also recolors the native OS title bar to match: a new pyRevitLabs.Common.DwmApi wrapper toggles DWMWA_USE_IMMERSIVE_DARK_MODE on every supported Windows version and additionally sets the exact caption/text colors on Windows 11 22000+. Both work runs are no-ops on older Windows. Title bar colors track live theme changes via a new ThemeChanged subscription; existing form XAMLs were switched from hardcoded White/Black/DimGray/etc. to those DynamicResource brushes, and the duplicated DatePicker ControlTemplate in GetValueWindow.xaml was dropped in favor of the new themed style.
WPFWindow defaults _live_refresh_root_colors to False so a live ThemeChanged refresh does not clobber the literal transparent Background that HUD-style overlays (CommandSwitchWindow, SearchPrompt, ...) declare in their own XAML. WPFPanel overrides it to True: dockable panes stay open for the whole Revit session and never use the transparent-HUD pattern, so they need the live refresh to actually repaint their Background/Foreground. apply_theme was a thin wrapper that just delegated to setup_resources(set_root_colors=False); collapse it back into _on_theme_refresh so the per-subclass _live_refresh_root_colors attribute is the single source of truth.
|
Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔 |
There was a problem hiding this comment.
Pull request overview
This PR adds automatic light/dark theming to all pyRevit WPF forms. A new central Theme.xaml resource dictionary is merged into every WPFWindow/WPFPanel, and a semantic palette (resolved from Revit's active UITheme) is injected as pyRevit*Color/pyRevit*Brush resources by _WPFMixin.setup_resources. Built-in WPF controls are re-templated against those brushes, native title bars are re-skinned via a new DwmApi P/Invoke wrapper, and windows/panes subscribe to Revit's ThemeChanged event to repaint live. Existing first-party XAML is swept to replace hardcoded light-only colors with the new theme brushes.
Changes:
- New
Theme.xamlrestyles standard WPF controls;_ipy.pyresolves the palette, injects brushes, applies DWM title-bar colors, and wires liveThemeChangedrefresh. - New
pyRevitLabs.Common.DwmApiwrapper for immersive dark mode + caption/text colors. - First-party XAML (SelectFromList, GetValueWindow, Settings, Extensions, DevTools panels) swept from hardcoded colors to theme brushes.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
pyrevitlib/pyrevit/forms/Theme.xaml |
New resource dictionary re-templating standard controls against the semantic palette |
pyrevitlib/pyrevit/forms/_ipy.py |
Palette definition, resource injection, DWM title-bar theming, live ThemeChanged subscribe/refresh |
pyrevitlib/pyrevit/forms/SelectFromList.xaml |
Removed light-only styles; icons bound to theme foreground/brushes |
pyrevitlib/pyrevit/forms/GetValueWindow.xaml |
Dropped hardcoded DatePicker template in favor of theme brushes |
dev/pyRevitLabs/pyRevitLabs.Common/Unmanaged.cs |
New DwmApi wrapper (immersive dark mode + caption/text colors) |
extensions/.../SettingsWindow.xaml |
Swept hardcoded colors to theme brushes; styles BasedOn theme defaults |
extensions/.../ExtensionsWindow.xaml |
Swept hardcoded colors; list/checkbox styles BasedOn theme defaults |
extensions/pyRevitDevTools.extension/SamplePanel.xaml |
Removed Background="White"; styles BasedOn theme defaults |
extensions/pyRevitDevTools.extension/DockableExample.xaml |
Removed hardcoded Background="White" |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| <ContentPresenter x:Name="content" | ||
| IsHitTestVisible="False" | ||
| Margin="{TemplateBinding Padding}" | ||
| HorizontalAlignment="Left" VerticalAlignment="Center" | ||
| Content="{Binding SelectionBoxItem, RelativeSource={RelativeSource TemplatedParent}}" | ||
| ContentTemplate="{Binding SelectionBoxItemTemplate, RelativeSource={RelativeSource TemplatedParent}}" | ||
| ContentTemplateSelector="{Binding ItemTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}"/> |
Add the editable text box template part to ComboBox so the editor surface picks up the theme brushes, and replace the custom ListViewItem template with style triggers that bind directly to pyRevit theme brushes.
Summary
Theme.xamlresource dictionary injects a semantic light/dark palette (resolved from Revit's activeUITheme) into everyWPFWindow/WPFPanel, and re-templates the standard WPF controls (Button,TextBox,ComboBox,ListBox/ListView,CheckBox,RadioButton,Expander,GroupBox,TabItem,ScrollBar,ContextMenu,MenuItem,ToolTip,Separator,Label) against it, so both first-party dialogs (Settings, Extensions, list pickers, etc.) and third-party extension forms/dockable panels pick up the theme automatically.pyRevitLabs.Common.DwmApiwrapper (DWMWA_USE_IMMERSIVE_DARK_MODEon any supported Windows version, exact caption/text colors on Windows 11 22000+), rather than replacing OS chrome with custom-drawn chrome — keeps drag/resize/snap/min/max/close fully native.ThemeChangedevent and repaint in place when the user toggles Revit's theme, no reload required.forms/,pyRevitCore.extension, andpyRevitDevTools.extensionsample panels was swept for hardcoded light-only colors (White/Black/DimGray/etc.) and switched to the new theme brushes.Test plan