Skip to content

Commit

Permalink
Experimental dark mode support
Browse files Browse the repository at this point in the history
Pulled from https://github.com/dotnet/winforms/pull/10985/files. Styling features and non-critical changes were stripped out to make this the smallest change possible.
  • Loading branch information
JeremyKuhne committed Aug 11, 2024
1 parent 1ff332e commit b238485
Show file tree
Hide file tree
Showing 63 changed files with 1,690 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<AssemblyName>Microsoft.VisualBasic.Forms</AssemblyName>
<Deterministic>true</Deterministic>
<OptionExplicit>On</OptionExplicit>
<OptionInfer>Off</OptionInfer>
<OptionStrict>On</OptionStrict>
<RootNamespace></RootNamespace>
<LangVersion>Latest</LangVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
' The .NET Foundation licenses this file to you under the MIT license.

Imports System.ComponentModel
Imports System.Diagnostics.CodeAnalysis
Imports System.Drawing
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Imports System.Windows.Forms.Analyzers.Diagnostics

Namespace Microsoft.VisualBasic.ApplicationServices

Expand All @@ -15,12 +17,20 @@ Namespace Microsoft.VisualBasic.ApplicationServices
Public Class ApplyApplicationDefaultsEventArgs
Inherits EventArgs

#Disable Warning WFO5001

Friend Sub New(minimumSplashScreenDisplayTime As Integer,
highDpiMode As HighDpiMode)
highDpiMode As HighDpiMode,
colorMode As SystemColorMode)

Me.MinimumSplashScreenDisplayTime = minimumSplashScreenDisplayTime
Me.HighDpiMode = highDpiMode
Me.ColorMode = colorMode
End Sub

#Enable Warning WFO5000
#Enable Warning WFO5001

''' <summary>
''' Setting this property inside the event handler causes a new default Font for Forms and UserControls to be set.
''' </summary>
Expand All @@ -34,7 +44,7 @@ Namespace Microsoft.VisualBasic.ApplicationServices
''' Setting this Property inside the event handler determines how long an application's Splash dialog is displayed at a minimum.
''' </summary>
Public Property MinimumSplashScreenDisplayTime As Integer =
WindowsFormsApplicationBase.MINIMUM_SPLASH_EXPOSURE_DEFAULT
WindowsFormsApplicationBase.MinimumSplashExposureDefault

''' <summary>
''' Setting this Property inside the event handler determines the general HighDpiMode for the application.
Expand All @@ -44,5 +54,12 @@ Namespace Microsoft.VisualBasic.ApplicationServices
''' </remarks>
Public Property HighDpiMode As HighDpiMode

''' <summary>
''' Setting this property inside the event handler determines the <see cref="Application.ColorMode"/> for the application.
''' </summary>
<Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat:=WindowsFormsApplicationBase.WinFormsExperimentalUrl)>
Public Property ColorMode As SystemColorMode

End Class
#Disable Warning WFO5001
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports System.Diagnostics.CodeAnalysis
Imports System.IO.Pipes
Imports System.Reflection
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Imports System.Security
Imports System.Threading
Imports System.Windows.Forms
Imports System.Windows.Forms.Analyzers.Diagnostics

Imports ExUtils = Microsoft.VisualBasic.CompilerServices.ExceptionUtils
Imports VbUtils = Microsoft.VisualBasic.CompilerServices.Utils
Expand Down Expand Up @@ -88,9 +90,10 @@ Namespace Microsoft.VisualBasic.ApplicationServices
Private Delegate Sub DisposeDelegate()

' How long a subsequent instance will wait for the original instance to get on its feet.
Private Const SECOND_INSTANCE_TIMEOUT As Integer = 2500 ' milliseconds.
Private Const SecondInstanceTimeOut As Integer = 2500 ' milliseconds.

Friend Const MINIMUM_SPLASH_EXPOSURE_DEFAULT As Integer = 2000 ' milliseconds.
Friend Const MinimumSplashExposureDefault As Integer = 2000 ' milliseconds.
Friend Const WinFormsExperimentalUrl As String = "https://aka.ms/winforms-experimental/{0}"

Private ReadOnly _splashLock As New Object
Private ReadOnly _appContext As WinFormsAppContext
Expand Down Expand Up @@ -131,15 +134,20 @@ Namespace Microsoft.VisualBasic.ApplicationServices
Private _splashScreen As Form

' Minimum amount of time to show the splash screen. 0 means hide as soon as the app comes up.
Private _minimumSplashExposure As Integer = MINIMUM_SPLASH_EXPOSURE_DEFAULT
Private _minimumSplashExposure As Integer = MinimumSplashExposureDefault
Private _splashTimer As Timers.Timer
Private _appSynchronizationContext As SynchronizationContext

' Informs My.Settings whether to save the settings on exit or not.
Private _saveMySettingsOnExit As Boolean

' The HighDpiMode the user picked from the AppDesigner or assigned to the ApplyHighDpiMode's Event.
' The HighDpiMode the user picked from the AppDesigner or assigned to the ApplyApplicationsDefault event.
Private _highDpiMode As HighDpiMode = HighDpiMode.SystemAware
#Disable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates.
' The ColorMode (Classic/Light, System, Dark) the user assigned to the ApplyApplicationsDefault event.
' Note: We aim to expose this to the App Designer in later runtime/VS versions.
Private _colorMode As SystemColorMode = SystemColorMode.Classic
#Enable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates.

''' <summary>
''' Occurs when the network availability changes.
Expand Down Expand Up @@ -249,15 +257,15 @@ Namespace Microsoft.VisualBasic.ApplicationServices
RaiseEvent(sender As Object, e As UnhandledExceptionEventArgs)
If _unhandledExceptionHandlers IsNot Nothing Then

' In the case that we throw from the <see cref="UnhandledException"/> handler, we don't want to
' run the <see cref="UnhandledException"/> handler again.
' In the case that we throw from the UnhandledException handler, we don't want to
' run the UnhandledException handler again.
_processingUnhandledExceptionEvent = True

For Each handler As UnhandledExceptionEventHandler In _unhandledExceptionHandlers
handler?.Invoke(sender, e)
Next

' Now that we are out of the <see cref="UnhandledException"/> handler, treat exceptions normally again.
' Now that we are out of the UnhandledException handler, treat exceptions normally again.
_processingUnhandledExceptionEvent = False
End If
End RaiseEvent
Expand Down Expand Up @@ -291,8 +299,8 @@ Namespace Microsoft.VisualBasic.ApplicationServices
If authenticationMode = AuthenticationMode.Windows Then
Try
' Consider: Sadly, a call to: System.Security.SecurityManager.IsGranted(New SecurityPermission(SecurityPermissionFlag.ControlPrincipal))
' Will only check THIS caller so you'll always get TRUE.
' What is needed is a way to get to the value of this on a demand basis.
' Will only check the THIS caller so you'll always get TRUE.
' What we need is a way to get to the value of this on a demand basis.
' So I try/catch instead for now but would rather be able to IF my way around this block.
Thread.CurrentPrincipal = New Principal.WindowsPrincipal(Principal.WindowsIdentity.GetCurrent)
Catch ex As SecurityException
Expand Down Expand Up @@ -347,9 +355,13 @@ Namespace Microsoft.VisualBasic.ApplicationServices

' --- We are launching a subsequent instance.
Dim tokenSource As New CancellationTokenSource()
tokenSource.CancelAfter(SECOND_INSTANCE_TIMEOUT)
tokenSource.CancelAfter(SecondInstanceTimeOut)
Try
Dim awaitable As ConfiguredTaskAwaitable = SendSecondInstanceArgsAsync(applicationInstanceID, commandLine, cancellationToken:=tokenSource.Token).ConfigureAwait(False)
Dim awaitable As ConfiguredTaskAwaitable = SendSecondInstanceArgsAsync(
pipeName:=applicationInstanceID,
args:=commandLine,
cancellationToken:=tokenSource.Token).ConfigureAwait(False)

awaitable.GetAwaiter().GetResult()
Catch ex As Exception
Throw New CantStartSingleInstanceException()
Expand Down Expand Up @@ -492,20 +504,24 @@ Namespace Microsoft.VisualBasic.ApplicationServices
' in a derived class and setting `MyBase.MinimumSplashScreenDisplayTime` there.
' We are picking this (probably) changed value up, and pass it to the ApplyDefaultsEvents
' where it could be modified (again). So event wins over Override over default value (2 seconds).
' b) We feed the default HighDpiMode (SystemAware) to the EventArgs. With the introduction of
' the HighDpiMode property, we give Project System the chance to reflect the HighDpiMode
' in the App Designer UI and have it code-generated based on a modified Application.myapp, which
' would result it to be set in the derived constructor. (See the hidden file in the Solution Explorer
' "My Project\Application.myapp\Application.Designer.vb for how those UI-set values get applied.)
' b) We feed the defaults for HighDpiMode, ColorMode, VisualStylesMode to the EventArgs.
' With the introduction of the HighDpiMode property, we changed Project System the chance to reflect
' those default values in the App Designer UI and have it code-generated based on a modified
' Application.myapp, which would result it to be set in the derived constructor.
' (See the hidden file in the Solution Explorer "My Project\Application.myapp\Application.Designer.vb
' for how those UI-set values get applied.)
' Once all this is done, we give the User another chance to change the value by code through
' the ApplyDefaults event.
' Overriding MinimumSplashScreenDisplayTime needs still to keep working!
' Note: Overriding MinimumSplashScreenDisplayTime needs still to keep working!
#Disable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates.
Dim applicationDefaultsEventArgs As New ApplyApplicationDefaultsEventArgs(
MinimumSplashScreenDisplayTime,
HighDpiMode) With
HighDpiMode,
ColorMode) With
{
.MinimumSplashScreenDisplayTime = MinimumSplashScreenDisplayTime
}
#Enable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates.

RaiseEvent ApplyApplicationDefaults(Me, applicationDefaultsEventArgs)

Expand All @@ -521,17 +537,22 @@ Namespace Microsoft.VisualBasic.ApplicationServices

_highDpiMode = applicationDefaultsEventArgs.HighDpiMode

#Disable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates.

_colorMode = applicationDefaultsEventArgs.ColorMode

' Then, it's applying what we got back as HighDpiMode.
Dim dpiSetResult As Boolean = Application.SetHighDpiMode(_highDpiMode)

If dpiSetResult Then
_highDpiMode = Application.HighDpiMode
End If
Debug.Assert(dpiSetResult, "We could net set the HighDpiMode.")

' And finally we take care of EnableVisualStyles.
If _enableVisualStyles Then
Application.EnableVisualStyles()
End If
' Now, let's set VisualStyles and ColorMode:
Application.SetColorMode(_colorMode)

#Enable Warning WFO5001 ' Type is for evaluation purposes only and is subject to change or removal in future updates.

' We'll handle "/nosplash" for you.
If Not (commandLineArgs.Contains("/nosplash") OrElse Me.CommandLineArgs.Contains("-nosplash")) Then
Expand Down Expand Up @@ -559,7 +580,7 @@ Namespace Microsoft.VisualBasic.ApplicationServices
' It is important not to create the network object until the ExecutionContext has everything on it.
' By now the principal will be on the thread so we can create the network object.
' The timing is important because the network object has an AsyncOperationsManager in it that marshals
' the network changed event to the main thread. The asycnOperationsManager does a CreateOperation()
' the network changed event to the main thread. The asyncOperationsManager does a CreateOperation()
' which makes a copy of the executionContext. That execution context shows up on your thread during
' the callback so I delay creating the network object (and consequently the capturing of the execution context)
' until the principal has been set on the thread. This avoids the problem where My.User isn't set
Expand Down Expand Up @@ -796,6 +817,23 @@ Namespace Microsoft.VisualBasic.ApplicationServices
End Set
End Property

''' <summary>
''' Gets or sets the ColorMode for the Application.
''' </summary>
''' <returns>
''' The <see cref="SystemColorMode"/> that the application is running in.
''' </returns>
<Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat:=WinFormsExperimentalUrl)>
<EditorBrowsable(EditorBrowsableState.Never)>
Protected Property ColorMode As SystemColorMode
Get
Return _colorMode
End Get
Set(value As SystemColorMode)
_colorMode = value
End Set
End Property

<EditorBrowsable(EditorBrowsableState.Advanced)>
Protected Property IsSingleInstance() As Boolean
Get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Imports Microsoft.VisualBasic.CompilerServices

Imports ExUtils = Microsoft.VisualBasic.CompilerServices.ExceptionUtils
Imports VbUtils = Microsoft.VisualBasic.CompilerServices.Utils
Imports NativeMethods = Microsoft.VisualBasic.CompilerServices.NativeMethods

Namespace Microsoft.VisualBasic

Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Microsoft.VisualBasic.ApplicationServices.ApplyApplicationDefaultsEventArgs.ColorMode() -> System.Windows.Forms.SystemColorMode
Microsoft.VisualBasic.ApplicationServices.ApplyApplicationDefaultsEventArgs.ColorMode(AutoPropertyValue As System.Windows.Forms.SystemColorMode) -> Void
Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.ColorMode() -> System.Windows.Forms.SystemColorMode
Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.ColorMode(value As System.Windows.Forms.SystemColorMode) -> Void
20 changes: 19 additions & 1 deletion src/System.Drawing.Common/src/System/Drawing/SystemBrushes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ namespace System.Drawing;

public static class SystemBrushes
{
#if NET9_0_OR_GREATER
#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
private static bool s_colorSetOnLastAccess = SystemColors.UseAlternativeColorSet;
#pragma warning restore SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#endif
private static readonly object s_systemBrushesKey = new();

public static Brush ActiveBorder => FromSystemColor(SystemColors.ActiveBorder);
Expand Down Expand Up @@ -57,7 +62,20 @@ public static Brush FromSystemColor(Color c)
throw new ArgumentException(SR.Format(SR.ColorNotSystemColor, c.ToString()));
}

if (!Gdip.ThreadData.TryGetValue(s_systemBrushesKey, out object? tempSystemBrushes) || tempSystemBrushes is not Brush[] systemBrushes)
#if NET9_0_OR_GREATER
#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (s_colorSetOnLastAccess != SystemColors.UseAlternativeColorSet)
{
s_colorSetOnLastAccess = SystemColors.UseAlternativeColorSet;

// We need to clear the SystemBrushes cache, when the ColorMode had changed.
Gdip.ThreadData.Remove(s_systemBrushesKey);
}
#pragma warning restore SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#endif

if (!Gdip.ThreadData.TryGetValue(s_systemBrushesKey, out object? tempSystemBrushes)
|| tempSystemBrushes is not Brush[] systemBrushes)
{
systemBrushes = new Brush[(int)KnownColor.WindowText + (int)KnownColor.MenuHighlight - (int)KnownColor.YellowGreen];
Gdip.ThreadData[s_systemBrushesKey] = systemBrushes;
Expand Down
21 changes: 20 additions & 1 deletion src/System.Drawing.Common/src/System/Drawing/SystemPens.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ namespace System.Drawing;

public static class SystemPens
{
#if NET9_0_OR_GREATER
#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
private static bool s_colorSetOnLastAccess = SystemColors.UseAlternativeColorSet;
#pragma warning restore SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#endif

private static readonly object s_systemPensKey = new();

public static Pen ActiveBorder => FromSystemColor(SystemColors.ActiveBorder);
Expand Down Expand Up @@ -58,7 +64,20 @@ public static Pen FromSystemColor(Color c)
throw new ArgumentException(SR.Format(SR.ColorNotSystemColor, c.ToString()));
}

if (!Gdip.ThreadData.TryGetValue(s_systemPensKey, out object? tempSystemPens) || tempSystemPens is not Pen[] systemPens)
#if NET9_0_OR_GREATER
#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (s_colorSetOnLastAccess != SystemColors.UseAlternativeColorSet)
{
s_colorSetOnLastAccess = SystemColors.UseAlternativeColorSet;

// We need to clear the SystemBrushes cache, when the ColorMode had changed.
Gdip.ThreadData.Remove(s_systemPensKey);
}
#pragma warning restore SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#endif

if (!Gdip.ThreadData.TryGetValue(s_systemPensKey, out object? tempSystemPens)
|| tempSystemPens is not Pen[] systemPens)
{
systemPens = new Pen[(int)KnownColor.WindowText + (int)KnownColor.MenuHighlight - (int)KnownColor.YellowGreen];
Gdip.ThreadData[s_systemPensKey] = systemPens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,4 @@
<data name="AddDesignerSerializationVisibilityCodeFixTitle" xml:space="preserve">
<value>Add DesignerSerializationVisibilityAttribute to property</value>
</data>
</root>
</root>
4 changes: 4 additions & 0 deletions src/System.Windows.Forms.Primitives/src/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ DSH_FLAGS
DTM_*
DTN_*
DTS_*
DwmGetWindowAttribute
DwmSetWindowAttribute
DWM_WINDOW_CORNER_PREFERENCE
DuplicateHandle
EC_*
ECO_*
Expand Down Expand Up @@ -164,6 +167,7 @@ GetClipboardFormatName
GetClipBox
GetClipCursor
GetClipRgn
GetComboBoxInfo
GetCurrentActCtx
GetCurrentObject
GetCurrentProcess
Expand Down
Loading

0 comments on commit b238485

Please sign in to comment.