11using System . Diagnostics ;
22using System . Windows ;
3- using System . Windows . Controls ;
43using System . Windows . Media ;
54using RevitDevTool . Models ;
65using Serilog ;
76using Serilog . Core ;
87using Serilog . Events ;
9- using Serilog . Sinks . RichTextBox . Themes ;
8+ using System . Windows . Forms . Integration ;
9+ using Wpf . Ui . Appearance ;
1010
1111namespace RevitDevTool . ViewModel ;
1212
1313internal partial class TraceLogViewModel : ObservableObject , IDisposable
1414{
15- public RichTextBox LogTextBox { get ; }
15+ public WindowsFormsHost LogTextBox { get ; }
1616
1717 private readonly LoggingLevelSwitch _levelSwitch ;
18- private readonly SerilogTraceListener _traceListener ;
1918 private readonly ConsoleRedirector _consoleRedirector ;
19+
20+ private SerilogTraceListener ? _traceListener ;
21+ private Logger ? _logger ;
22+
23+ private readonly RichTextBox _winFormsTextBox ;
24+ private readonly FrameworkElement _resourceOwner ;
25+ private bool _forceMonoOnLight ;
26+ private System . Drawing . Color _currentForeColor = System . Drawing . Color . Black ;
27+
2028
2129 [ ObservableProperty ] private bool _isStarted = true ;
2230 [ ObservableProperty ] private LogEventLevel _logLevel = LogEventLevel . Debug ;
@@ -35,7 +43,8 @@ private void TraceStatus(bool isStarted)
3543 {
3644 if ( isStarted )
3745 {
38- Trace . Listeners . Add ( _traceListener ) ;
46+ Initialized ( ) ;
47+ Trace . Listeners . Add ( _traceListener ! ) ;
3948 Trace . Listeners . Add ( TraceGeometry . TraceListener ) ;
4049 VisualizationController . Start ( ) ;
4150 }
@@ -44,35 +53,152 @@ private void TraceStatus(bool isStarted)
4453 Trace . Listeners . Remove ( _traceListener ) ;
4554 Trace . Listeners . Remove ( TraceGeometry . TraceListener ) ;
4655 VisualizationController . Stop ( ) ;
56+ CloseAndFlush ( ) ;
4757 }
4858 }
4959
50- public TraceLogViewModel ( )
60+ private void Initialized ( )
61+ {
62+ _logger ??= new LoggerConfiguration ( )
63+ . MinimumLevel . ControlledBy ( _levelSwitch )
64+ . WriteTo . RichTextBox ( _winFormsTextBox )
65+ . CreateLogger ( ) ;
66+ _traceListener ??= new SerilogTraceListener ( _logger ) ;
67+ }
68+
69+ private void CloseAndFlush ( )
70+ {
71+ _winFormsTextBox . Clear ( ) ;
72+ _logger ? . Dispose ( ) ;
73+ _traceListener ? . Dispose ( ) ;
74+ }
75+
76+
77+ public TraceLogViewModel ( FrameworkElement resourceOwner )
5178 {
52- LogTextBox = new RichTextBox
79+ _resourceOwner = resourceOwner ;
80+ _winFormsTextBox = new RichTextBox
5381 {
54- FontFamily = new FontFamily ( "Cascadia Mono, Consolas, Courier New, monospace" ) ,
55- VerticalScrollBarVisibility = ScrollBarVisibility . Auto ,
56- VerticalContentAlignment = VerticalAlignment . Top ,
57- IsReadOnly = true ,
82+ Font = new Font ( "Cascadia Mono" , 9f , System . Drawing . FontStyle . Regular , GraphicsUnit . Point ) ,
83+ ReadOnly = true ,
84+ DetectUrls = true ,
85+ WordWrap = true ,
86+ ScrollBars = RichTextBoxScrollBars . Vertical ,
87+ BorderStyle = BorderStyle . None
5888 } ;
89+
90+ LogTextBox = new WindowsFormsHost
91+ {
92+ Child = _winFormsTextBox
93+ } ;
94+
95+ _winFormsTextBox . TextChanged += OnWinFormsTextChanged ;
5996
6097 PresentationTraceSources . ResourceDictionarySource . Switch . Level = SourceLevels . Critical ;
6198 _levelSwitch = new LoggingLevelSwitch ( _logLevel ) ;
62-
63- var logger = new LoggerConfiguration ( )
64- . MinimumLevel . ControlledBy ( _levelSwitch )
65- . WriteTo . RichTextBox ( LogTextBox , theme : RichTextBoxConsoleTheme . Colored )
66- . CreateLogger ( ) ;
67-
68- _traceListener = new SerilogTraceListener ( logger ) ;
6999 _consoleRedirector = new ConsoleRedirector ( ) ;
100+
101+ ApplicationThemeManager . Changed += OnThemeChanged ;
70102 TraceStatus ( IsStarted ) ;
71103 }
72104
105+ private void OnThemeChanged ( ApplicationTheme theme , System . Windows . Media . Color accent )
106+ {
107+ LogTextBox . Dispatcher . Invoke ( ApplyThemeToLogTextBox ) ;
108+ }
109+
110+ private void ApplyThemeToLogTextBox ( )
111+ {
112+ var bgBrush = _resourceOwner . TryFindResource ( "SolidBackgroundFillColorBaseBrush" ) as SolidColorBrush ;
113+ var fgBrush = _resourceOwner . TryFindResource ( "TextFillColorPrimaryBrush" ) as SolidColorBrush ;
114+
115+ System . Drawing . Color ? back = null ;
116+ System . Drawing . Color ? fore = null ;
117+
118+ if ( bgBrush is not null )
119+ {
120+ var c = bgBrush . Color ;
121+ back = System . Drawing . Color . FromArgb ( c . A , c . R , c . G , c . B ) ;
122+ _winFormsTextBox . BackColor = back . Value ;
123+ }
124+ if ( fgBrush is not null )
125+ {
126+ var c = fgBrush . Color ;
127+ fore = System . Drawing . Color . FromArgb ( c . A , c . R , c . G , c . B ) ;
128+ }
129+
130+ // Improve readability for light themes: fallback to Black if the selected foreground is too light
131+ if ( back is { } b )
132+ {
133+ var isDark = IsDark ( b ) ;
134+ if ( fore is { } f )
135+ {
136+ if ( ! isDark && IsTooLight ( f ) )
137+ {
138+ f = System . Drawing . Color . Black ;
139+ }
140+ _winFormsTextBox . ForeColor = f ;
141+ _currentForeColor = f ;
142+ // In light mode, enforce readable mono color over any fragment coloring produced by the sink
143+ _forceMonoOnLight = ! isDark ;
144+ if ( _forceMonoOnLight )
145+ {
146+ ForceRichTextForeColor ( f ) ;
147+ }
148+ else
149+ {
150+ _forceMonoOnLight = false ;
151+ }
152+ }
153+
154+ // Try to match scrollbars to theme (Win11/10) via Immersive Dark Mode for dark;
155+ // use Explorer theme in light for modern scrollbars (handled inside helper)
156+ Win32DarkMode . SetImmersiveDarkMode ( _winFormsTextBox . Handle , isDark ) ;
157+ }
158+ }
159+
160+ private static bool IsDark ( System . Drawing . Color c )
161+ {
162+ var lum = ( 0.2126 * c . R + 0.7152 * c . G + 0.0722 * c . B ) / 255.0 ;
163+ return lum < 0.5 ;
164+ }
165+
166+ private static bool IsTooLight ( System . Drawing . Color c )
167+ {
168+ var lum = ( 0.2126 * c . R + 0.7152 * c . G + 0.0722 * c . B ) / 255.0 ;
169+ return lum > 0.75 ; // very light
170+ }
171+
172+ private void ForceRichTextForeColor ( System . Drawing . Color color )
173+ {
174+ try
175+ {
176+ _winFormsTextBox . SuspendLayout ( ) ;
177+ var savedStart = _winFormsTextBox . SelectionStart ;
178+ var savedLength = _winFormsTextBox . SelectionLength ;
179+ _winFormsTextBox . SelectAll ( ) ;
180+ _winFormsTextBox . SelectionColor = color ;
181+ _winFormsTextBox . SelectionStart = savedStart ;
182+ _winFormsTextBox . SelectionLength = savedLength ;
183+ }
184+ finally
185+ {
186+ _winFormsTextBox . ResumeLayout ( ) ;
187+ }
188+ }
189+
190+ private void OnWinFormsTextChanged ( object ? sender , EventArgs e )
191+ {
192+ if ( _forceMonoOnLight )
193+ {
194+ ForceRichTextForeColor ( _currentForeColor ) ;
195+ }
196+ }
197+
73198 [ RelayCommand ] private void Clear ( )
74199 {
75- LogTextBox . Document . Blocks . Clear ( ) ;
200+ CloseAndFlush ( ) ;
201+ Initialized ( ) ;
76202 }
77203
78204 [ RelayCommand ] private static void ClearGeometry ( )
@@ -82,7 +208,45 @@ [RelayCommand] private static void ClearGeometry()
82208
83209 public void Dispose ( )
84210 {
211+ ApplicationThemeManager . Changed -= OnThemeChanged ;
212+ _winFormsTextBox . TextChanged -= OnWinFormsTextChanged ;
85213 _consoleRedirector . Dispose ( ) ;
86214 GC . SuppressFinalize ( this ) ;
87215 }
216+
217+ public void RefreshTheme ( )
218+ {
219+ ApplyThemeToLogTextBox ( ) ;
220+ }
221+ }
222+
223+ internal static class Win32DarkMode
224+ {
225+ private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 ;
226+ private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20 ;
227+
228+ [ System . Runtime . InteropServices . DllImport ( "dwmapi.dll" , CharSet = System . Runtime . InteropServices . CharSet . Unicode , SetLastError = true ) ]
229+ private static extern int DwmSetWindowAttribute ( IntPtr hwnd , int attr , ref int attrValue , int attrSize ) ;
230+
231+ [ System . Runtime . InteropServices . DllImport ( "uxtheme.dll" , CharSet = System . Runtime . InteropServices . CharSet . Unicode , SetLastError = true ) ]
232+ private static extern int SetWindowTheme ( IntPtr hWnd , string pszSubAppName , string pszSubIdList ) ;
233+
234+ public static void SetImmersiveDarkMode ( IntPtr hwnd , bool enable )
235+ {
236+ if ( hwnd == IntPtr . Zero ) return ;
237+ var useDark = enable ? 1 : 0 ;
238+ _ = DwmSetWindowAttribute ( hwnd , DWMWA_USE_IMMERSIVE_DARK_MODE , ref useDark , sizeof ( int ) ) ;
239+ _ = DwmSetWindowAttribute ( hwnd , DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 , ref useDark , sizeof ( int ) ) ;
240+ try
241+ {
242+ if ( enable )
243+ SetWindowTheme ( hwnd , "DarkMode_Explorer" , null ) ;
244+ else
245+ SetWindowTheme ( hwnd , "Explorer" , null ) ;
246+ }
247+ catch
248+ {
249+ // ignore if not supported
250+ }
251+ }
88252}
0 commit comments