Skip to content

Commit 3ba6fa4

Browse files
committed
refactor(ui): update TraceLog to use WindowsForms RichTextBox and improve theme handling
- Replaced WPF RichTextBox with WindowsForms RichTextBox for better performance. - Enhanced theme management to dynamically adjust the log text box appearance based on the application theme. - Streamlined initialization and disposal processes for better resource management.
1 parent ea6d1c9 commit 3ba6fa4

File tree

6 files changed

+207
-29
lines changed

6 files changed

+207
-29
lines changed

source/RevitDevTool/Commands/TraceCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public override void Execute()
2626
}
2727
catch (Exception e)
2828
{
29-
TaskDialog.Show("Error", e.ToString());
29+
Autodesk.Revit.UI.TaskDialog.Show("Error", e.ToString());
3030
}
3131
}
3232

source/RevitDevTool/RevitDevTool.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<UseWPF>true</UseWPF>
5+
<UseWindowsForms>true</UseWindowsForms>
56
<Nullable>enable</Nullable>
67
<LangVersion>latest</LangVersion>
78
<PlatformTarget>x64</PlatformTarget>
@@ -63,7 +64,7 @@
6364
<PackageReference Include="ILRepack" Version="2.0.41" />
6465

6566
<!-- Logging -->
66-
<PackageReference Include="Serilog.Sinks.RichTextBox.Wpf" Version="1.1.0" />
67+
<PackageReference Include="Serilog.Sinks.RichTextBox.WinForms.Colored" Version="3.1.3" />
6768
</ItemGroup>
6869

6970
<ItemGroup>

source/RevitDevTool/View/TraceLog.xaml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
xmlns:sys="clr-namespace:System;assembly=mscorlib"
99
xmlns:ui="http://revitdevtool.com/xaml"
1010
xmlns:vm="clr-namespace:RevitDevTool.ViewModel"
11+
d:DataContext="{d:DesignInstance vm:TraceLogViewModel}"
1112
d:DesignHeight="600"
1213
d:DesignWidth="450"
1314
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
@@ -27,11 +28,9 @@
2728
</ResourceDictionary>
2829
</Page.Resources>
2930

30-
<Page.DataContext>
31-
<vm:TraceLogViewModel />
32-
</Page.DataContext>
33-
34-
<DockPanel>
31+
<DockPanel
32+
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
33+
ScrollViewer.VerticalScrollBarVisibility="Disabled">
3534
<DockPanel
3635
Margin="5"
3736
DockPanel.Dock="Top">
@@ -60,8 +59,15 @@
6059
</StackPanel>
6160
</DockPanel>
6261

63-
<ContentControl
62+
<Border
6463
Margin="5"
65-
Content="{Binding LogTextBox}" />
64+
Padding="2"
65+
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
66+
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
67+
BorderThickness="1"
68+
CornerRadius="6"
69+
SnapsToDevicePixels="True">
70+
<ContentControl Content="{Binding LogTextBox}" />
71+
</Border>
6672
</DockPanel>
6773
</Page>

source/RevitDevTool/View/TraceLog.xaml.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using RevitDevTool.Theme;
2+
using RevitDevTool.ViewModel;
23
using ApplicationTheme = UIFramework.ApplicationTheme;
34
#if REVIT2024_OR_GREATER
45
using System.ComponentModel;
@@ -14,12 +15,18 @@ public partial class TraceLog : IDisposable
1415
public TraceLog()
1516
{
1617
InitializeComponent();
18+
DataContext = new TraceLogViewModel(this);
1719
#if REVIT2024_OR_GREATER
1820
ApplicationTheme.CurrentTheme.PropertyChanged += ApplyTheme;
1921
#endif
2022
ThemeWatcher.Initialize();
2123
ThemeWatcher.Watch(this);
2224
ThemeWatcher.ApplyTheme();
25+
Loaded += (_, _) =>
26+
{
27+
if (DataContext is TraceLogViewModel vm)
28+
vm.RefreshTheme();
29+
};
2330
}
2431

2532
#if REVIT2024_OR_GREATER
Lines changed: 183 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
using System.Diagnostics;
22
using System.Windows;
3-
using System.Windows.Controls;
43
using System.Windows.Media;
54
using RevitDevTool.Models;
65
using Serilog;
76
using Serilog.Core;
87
using Serilog.Events;
9-
using Serilog.Sinks.RichTextBox.Themes;
8+
using System.Windows.Forms.Integration;
9+
using Wpf.Ui.Appearance;
1010

1111
namespace RevitDevTool.ViewModel;
1212

1313
internal 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
}

source/RevitDevTool/Visualization/Helpers/RenderHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ public static void MapBoundingBoxSurfaceBuffer(RenderingBufferStorage buffer, Bo
375375
];
376376

377377
// Transform each corner individually to world coordinates
378-
XYZ[] corners = localCorners
378+
var corners = localCorners
379379
.Select(corner => box.Transform.OfPoint(corner))
380380
.ToArray();
381381

0 commit comments

Comments
 (0)