2025-09-18 03:58:38 +00:00
using System ;
2025-04-17 23:39:02 +00:00
using System.Collections.Generic ;
2025-03-16 14:24:06 +00:00
using System.ComponentModel ;
2025-04-11 03:08:28 +00:00
using System.Diagnostics ;
2025-03-23 14:22:08 +00:00
using System.Globalization ;
2025-05-27 06:36:09 +00:00
using System.IO ;
2025-04-17 23:39:02 +00:00
using System.Linq ;
2024-12-24 03:59:04 +00:00
using System.Runtime.InteropServices ;
2025-05-02 04:26:14 +00:00
using System.Threading ;
using System.Threading.Tasks ;
2024-12-24 03:59:04 +00:00
using System.Windows ;
2025-03-16 14:24:06 +00:00
using System.Windows.Interop ;
2025-04-17 23:39:02 +00:00
using System.Windows.Markup ;
2025-03-16 14:24:06 +00:00
using System.Windows.Media ;
2025-03-23 14:22:08 +00:00
using Flow.Launcher.Infrastructure.UserSettings ;
2025-07-23 05:51:35 +00:00
using Flow.Launcher.Plugin.SharedModels ;
2025-03-23 14:22:08 +00:00
using Microsoft.Win32 ;
2025-07-20 11:11:09 +00:00
using Microsoft.Win32.SafeHandles ;
2025-02-24 09:01:01 +00:00
using Windows.Win32 ;
2025-03-16 14:24:06 +00:00
using Windows.Win32.Foundation ;
2025-02-24 09:01:01 +00:00
using Windows.Win32.Graphics.Dwm ;
2025-09-30 13:11:03 +00:00
using Windows.Win32.System.Power ;
2025-07-20 11:11:09 +00:00
using Windows.Win32.System.Threading ;
2025-03-22 04:06:44 +00:00
using Windows.Win32.UI.Input.KeyboardAndMouse ;
2025-05-27 06:36:09 +00:00
using Windows.Win32.UI.Shell.Common ;
2025-03-16 14:24:06 +00:00
using Windows.Win32.UI.WindowsAndMessaging ;
2025-03-22 04:06:44 +00:00
using Point = System . Windows . Point ;
2025-04-17 23:39:02 +00:00
using SystemFonts = System . Windows . SystemFonts ;
2024-12-19 08:34:32 +00:00
namespace Flow.Launcher.Infrastructure
{
public static class Win32Helper
{
2025-03-16 06:23:29 +00:00
#region Blur Handling
2025-03-16 06:46:39 +00:00
public static bool IsBackdropSupported ( )
{
2025-03-16 14:24:06 +00:00
// Mica and Acrylic only supported Windows 11 22000+
2025-03-16 06:46:39 +00:00
return RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) & &
2025-03-16 09:48:22 +00:00
Environment . OSVersion . Version . Build > = 22000 ;
}
public static unsafe bool DWMSetCloakForWindow ( Window window , bool cloak )
{
var cloaked = cloak ? 1 : 0 ;
return PInvoke . DwmSetWindowAttribute (
2025-03-16 14:24:06 +00:00
GetWindowHandle ( window ) ,
2025-03-16 09:48:22 +00:00
DWMWINDOWATTRIBUTE . DWMWA_CLOAK ,
& cloaked ,
( uint ) Marshal . SizeOf < int > ( ) ) . Succeeded ;
2025-03-16 06:46:39 +00:00
}
2025-03-16 06:23:29 +00:00
public static unsafe bool DWMSetBackdropForWindow ( Window window , BackdropTypes backdrop )
{
var backdropType = backdrop switch
{
BackdropTypes . Acrylic = > DWM_SYSTEMBACKDROP_TYPE . DWMSBT_TRANSIENTWINDOW ,
BackdropTypes . Mica = > DWM_SYSTEMBACKDROP_TYPE . DWMSBT_MAINWINDOW ,
BackdropTypes . MicaAlt = > DWM_SYSTEMBACKDROP_TYPE . DWMSBT_TABBEDWINDOW ,
_ = > DWM_SYSTEMBACKDROP_TYPE . DWMSBT_AUTO
} ;
return PInvoke . DwmSetWindowAttribute (
2025-03-16 14:24:06 +00:00
GetWindowHandle ( window ) ,
2025-03-16 06:23:29 +00:00
DWMWINDOWATTRIBUTE . DWMWA_SYSTEMBACKDROP_TYPE ,
& backdropType ,
( uint ) Marshal . SizeOf < int > ( ) ) . Succeeded ;
}
public static unsafe bool DWMSetDarkModeForWindow ( Window window , bool useDarkMode )
{
var darkMode = useDarkMode ? 1 : 0 ;
return PInvoke . DwmSetWindowAttribute (
2025-03-16 14:24:06 +00:00
GetWindowHandle ( window ) ,
2025-03-16 06:23:29 +00:00
DWMWINDOWATTRIBUTE . DWMWA_USE_IMMERSIVE_DARK_MODE ,
& darkMode ,
( uint ) Marshal . SizeOf < int > ( ) ) . Succeeded ;
}
2025-03-16 06:39:56 +00:00
/// <summary>
2025-03-22 08:58:42 +00:00
///
2025-03-16 06:39:56 +00:00
/// </summary>
/// <param name="window"></param>
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
/// <returns></returns>
public static unsafe bool DWMSetCornerPreferenceForWindow ( Window window , string cornerType )
{
var preference = cornerType switch
{
"DoNotRound" = > DWM_WINDOW_CORNER_PREFERENCE . DWMWCP_DONOTROUND ,
"Round" = > DWM_WINDOW_CORNER_PREFERENCE . DWMWCP_ROUND ,
"RoundSmall" = > DWM_WINDOW_CORNER_PREFERENCE . DWMWCP_ROUNDSMALL ,
"Default" = > DWM_WINDOW_CORNER_PREFERENCE . DWMWCP_DEFAULT ,
_ = > throw new InvalidOperationException ( "Invalid corner type" )
} ;
return PInvoke . DwmSetWindowAttribute (
2025-03-16 14:24:06 +00:00
GetWindowHandle ( window ) ,
2025-03-16 06:39:56 +00:00
DWMWINDOWATTRIBUTE . DWMWA_WINDOW_CORNER_PREFERENCE ,
& preference ,
( uint ) Marshal . SizeOf < int > ( ) ) . Succeeded ;
}
2025-03-16 06:23:29 +00:00
#endregion
2025-03-16 14:24:06 +00:00
#region Wallpaper
public static unsafe string GetWallpaperPath ( )
{
var wallpaperPtr = stackalloc char [ ( int ) PInvoke . MAX_PATH ] ;
PInvoke . SystemParametersInfo ( SYSTEM_PARAMETERS_INFO_ACTION . SPI_GETDESKWALLPAPER , PInvoke . MAX_PATH ,
wallpaperPtr ,
0 ) ;
var wallpaper = MemoryMarshal . CreateReadOnlySpanFromNullTerminated ( wallpaperPtr ) ;
return wallpaper . ToString ( ) ;
}
#endregion
#region Window Foreground
2025-07-19 12:26:33 +00:00
public static unsafe nint GetForegroundWindow ( )
2025-03-16 14:24:06 +00:00
{
2025-07-19 12:26:33 +00:00
return ( nint ) PInvoke . GetForegroundWindow ( ) . Value ;
2025-03-16 14:24:06 +00:00
}
public static bool SetForegroundWindow ( Window window )
{
return PInvoke . SetForegroundWindow ( GetWindowHandle ( window ) ) ;
}
public static bool SetForegroundWindow ( nint handle )
{
return PInvoke . SetForegroundWindow ( new ( handle ) ) ;
}
2025-03-24 00:51:24 +00:00
public static bool IsForegroundWindow ( Window window )
{
2025-03-30 01:12:14 +00:00
return IsForegroundWindow ( GetWindowHandle ( window ) ) ;
2025-03-24 00:51:24 +00:00
}
2025-07-20 11:11:09 +00:00
public static bool IsForegroundWindow ( nint handle )
{
return IsForegroundWindow ( new HWND ( handle ) ) ;
}
2025-03-24 00:51:24 +00:00
internal static bool IsForegroundWindow ( HWND handle )
{
return handle . Equals ( PInvoke . GetForegroundWindow ( ) ) ;
}
2025-03-16 14:24:06 +00:00
#endregion
#region Task Switching
/// <summary>
/// Hide windows in the Alt+Tab window list
/// </summary>
/// <param name="window">To hide a window</param>
public static void HideFromAltTab ( Window window )
{
var hwnd = GetWindowHandle ( window ) ;
2025-03-16 15:18:50 +00:00
var exStyle = GetWindowStyle ( hwnd , WINDOW_LONG_PTR_INDEX . GWL_EXSTYLE ) ;
2025-03-16 14:24:06 +00:00
// Add TOOLWINDOW style, remove APPWINDOW style
var newExStyle = ( ( uint ) exStyle | ( uint ) WINDOW_EX_STYLE . WS_EX_TOOLWINDOW ) & ~ ( uint ) WINDOW_EX_STYLE . WS_EX_APPWINDOW ;
2025-03-16 15:18:50 +00:00
SetWindowStyle ( hwnd , WINDOW_LONG_PTR_INDEX . GWL_EXSTYLE , ( int ) newExStyle ) ;
2025-03-16 14:24:06 +00:00
}
/// <summary>
/// Restore window display in the Alt+Tab window list.
/// </summary>
/// <param name="window">To restore the displayed window</param>
public static void ShowInAltTab ( Window window )
{
var hwnd = GetWindowHandle ( window ) ;
2025-03-16 15:18:50 +00:00
var exStyle = GetWindowStyle ( hwnd , WINDOW_LONG_PTR_INDEX . GWL_EXSTYLE ) ;
2025-03-16 14:24:06 +00:00
// Remove the TOOLWINDOW style and add the APPWINDOW style.
var newExStyle = ( ( uint ) exStyle & ~ ( uint ) WINDOW_EX_STYLE . WS_EX_TOOLWINDOW ) | ( uint ) WINDOW_EX_STYLE . WS_EX_APPWINDOW ;
2025-03-16 15:18:50 +00:00
SetWindowStyle ( GetWindowHandle ( window ) , WINDOW_LONG_PTR_INDEX . GWL_EXSTYLE , ( int ) newExStyle ) ;
2025-03-16 14:24:06 +00:00
}
/// <summary>
/// Disable windows toolbar's control box
/// This will also disable system menu with Alt+Space hotkey
/// </summary>
public static void DisableControlBox ( Window window )
{
var hwnd = GetWindowHandle ( window ) ;
2025-03-16 15:18:50 +00:00
var style = GetWindowStyle ( hwnd , WINDOW_LONG_PTR_INDEX . GWL_STYLE ) ;
2025-03-16 14:24:06 +00:00
style & = ~ ( int ) WINDOW_STYLE . WS_SYSMENU ;
2025-03-16 15:18:50 +00:00
SetWindowStyle ( hwnd , WINDOW_LONG_PTR_INDEX . GWL_STYLE , style ) ;
2025-03-16 14:24:06 +00:00
}
2025-05-24 05:10:25 +00:00
private static nint GetWindowStyle ( HWND hWnd , WINDOW_LONG_PTR_INDEX nIndex )
2025-03-16 14:24:06 +00:00
{
2025-05-24 05:10:25 +00:00
var style = PInvoke . GetWindowLongPtr ( hWnd , nIndex ) ;
2025-03-16 14:24:06 +00:00
if ( style = = 0 & & Marshal . GetLastPInvokeError ( ) ! = 0 )
{
throw new Win32Exception ( Marshal . GetLastPInvokeError ( ) ) ;
}
return style ;
}
2025-05-24 05:10:25 +00:00
private static nint SetWindowStyle ( HWND hWnd , WINDOW_LONG_PTR_INDEX nIndex , nint dwNewLong )
2025-03-16 14:24:06 +00:00
{
PInvoke . SetLastError ( WIN32_ERROR . NO_ERROR ) ; // Clear any existing error
2025-03-16 15:18:50 +00:00
var result = PInvoke . SetWindowLongPtr ( hWnd , nIndex , dwNewLong ) ;
2025-03-16 14:24:06 +00:00
if ( result = = 0 & & Marshal . GetLastPInvokeError ( ) ! = 0 )
{
throw new Win32Exception ( Marshal . GetLastPInvokeError ( ) ) ;
}
return result ;
}
#endregion
#region Window Fullscreen
private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass" ;
private const string WINDOW_CLASS_WINTAB = "Flip3D" ;
private const string WINDOW_CLASS_PROGMAN = "Progman" ;
private const string WINDOW_CLASS_WORKERW = "WorkerW" ;
private static HWND _hwnd_shell ;
private static HWND HWND_SHELL = >
_hwnd_shell ! = HWND . Null ? _hwnd_shell : _hwnd_shell = PInvoke . GetShellWindow ( ) ;
private static HWND _hwnd_desktop ;
private static HWND HWND_DESKTOP = >
_hwnd_desktop ! = HWND . Null ? _hwnd_desktop : _hwnd_desktop = PInvoke . GetDesktopWindow ( ) ;
public static unsafe bool IsForegroundWindowFullscreen ( )
{
// Get current active window
var hWnd = PInvoke . GetForegroundWindow ( ) ;
if ( hWnd . Equals ( HWND . Null ) )
{
return false ;
}
// If current active window is desktop or shell, exit early
if ( hWnd . Equals ( HWND_DESKTOP ) | | hWnd . Equals ( HWND_SHELL ) )
{
return false ;
}
string windowClass ;
const int capacity = 256 ;
Span < char > buffer = stackalloc char [ capacity ] ;
int validLength ;
fixed ( char * pBuffer = buffer )
{
validLength = PInvoke . GetClassName ( hWnd , pBuffer , capacity ) ;
}
windowClass = buffer [ . . validLength ] . ToString ( ) ;
// For Win+Tab (Flip3D)
if ( windowClass = = WINDOW_CLASS_WINTAB )
{
return false ;
}
PInvoke . GetWindowRect ( hWnd , out var appBounds ) ;
// For console (ConsoleWindowClass), we have to check for negative dimensions
if ( windowClass = = WINDOW_CLASS_CONSOLE )
{
return appBounds . top < 0 & & appBounds . bottom < 0 ;
}
// For desktop (Progman or WorkerW, depends on the system), we have to check
if ( windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW )
{
var hWndDesktop = PInvoke . FindWindowEx ( hWnd , HWND . Null , "SHELLDLL_DefView" , null ) ;
hWndDesktop = PInvoke . FindWindowEx ( hWndDesktop , HWND . Null , "SysListView32" , "FolderView" ) ;
2025-07-19 12:26:33 +00:00
if ( hWndDesktop ! = HWND . Null )
2025-03-16 14:24:06 +00:00
{
return false ;
}
}
var monitorInfo = MonitorInfo . GetNearestDisplayMonitor ( hWnd ) ;
2025-07-23 05:27:36 +00:00
return ( appBounds . bottom - appBounds . top ) = = monitorInfo . Bounds . Height & &
( appBounds . right - appBounds . left ) = = monitorInfo . Bounds . Width ;
2025-03-16 14:24:06 +00:00
}
#endregion
#region Pixel to DIP
/// <summary>
/// Transforms pixels to Device Independent Pixels used by WPF
/// </summary>
/// <param name="visual">current window, required to get presentation source</param>
/// <param name="unitX">horizontal position in pixels</param>
/// <param name="unitY">vertical position in pixels</param>
/// <returns>point containing device independent pixels</returns>
public static Point TransformPixelsToDIP ( Visual visual , double unitX , double unitY )
{
Matrix matrix ;
var source = PresentationSource . FromVisual ( visual ) ;
if ( source is not null )
{
matrix = source . CompositionTarget . TransformFromDevice ;
}
else
{
using var src = new HwndSource ( new HwndSourceParameters ( ) ) ;
matrix = src . CompositionTarget . TransformFromDevice ;
}
return new Point ( ( int ) ( matrix . M11 * unitX ) , ( int ) ( matrix . M22 * unitY ) ) ;
}
#endregion
#region WndProc
2025-03-19 10:03:02 +00:00
public const int WM_ENTERSIZEMOVE = ( int ) PInvoke . WM_ENTERSIZEMOVE ;
public const int WM_EXITSIZEMOVE = ( int ) PInvoke . WM_EXITSIZEMOVE ;
2025-06-05 08:12:55 +00:00
public const int WM_NCLBUTTONDBLCLK = ( int ) PInvoke . WM_NCLBUTTONDBLCLK ;
public const int WM_SYSCOMMAND = ( int ) PInvoke . WM_SYSCOMMAND ;
public const int SC_MAXIMIZE = ( int ) PInvoke . SC_MAXIMIZE ;
public const int SC_MINIMIZE = ( int ) PInvoke . SC_MINIMIZE ;
2025-03-16 14:24:06 +00:00
#endregion
#region Window Handle
2025-03-16 14:37:22 +00:00
internal static HWND GetWindowHandle ( Window window , bool ensure = false )
2025-03-16 14:24:06 +00:00
{
var windowHelper = new WindowInteropHelper ( window ) ;
2025-03-16 14:37:22 +00:00
if ( ensure )
{
windowHelper . EnsureHandle ( ) ;
}
2025-03-16 14:24:06 +00:00
return new ( windowHelper . Handle ) ;
}
2025-07-20 11:11:09 +00:00
internal static HWND GetMainWindowHandle ( )
{
// When application is exiting, the Application.Current will be null
if ( Application . Current = = null ) return HWND . Null ;
// Get the FL main window
var hwnd = GetWindowHandle ( Application . Current . MainWindow , true ) ;
return hwnd ;
}
2025-03-16 14:24:06 +00:00
#endregion
2025-03-22 04:06:44 +00:00
2025-05-02 04:26:14 +00:00
#region STA Thread
/ *
Inspired by https : //github.com/files-community/Files code on STA Thread handling.
* /
public static Task StartSTATaskAsync ( Action action )
{
var taskCompletionSource = new TaskCompletionSource ( ) ;
Thread thread = new ( ( ) = >
{
PInvoke . OleInitialize ( ) ;
try
{
action ( ) ;
taskCompletionSource . SetResult ( ) ;
}
catch ( System . Exception ex )
{
taskCompletionSource . SetException ( ex ) ;
}
finally
{
PInvoke . OleUninitialize ( ) ;
}
} )
{
IsBackground = true ,
Priority = ThreadPriority . Normal
} ;
thread . SetApartmentState ( ApartmentState . STA ) ;
thread . Start ( ) ;
return taskCompletionSource . Task ;
}
public static Task < T > StartSTATaskAsync < T > ( Func < T > func )
{
var taskCompletionSource = new TaskCompletionSource < T > ( ) ;
Thread thread = new ( ( ) = >
{
PInvoke . OleInitialize ( ) ;
try
{
taskCompletionSource . SetResult ( func ( ) ) ;
}
catch ( System . Exception ex )
{
taskCompletionSource . SetException ( ex ) ;
}
finally
{
PInvoke . OleUninitialize ( ) ;
}
} )
{
IsBackground = true ,
Priority = ThreadPriority . Normal
} ;
thread . SetApartmentState ( ApartmentState . STA ) ;
thread . Start ( ) ;
return taskCompletionSource . Task ;
}
#endregion
2025-03-22 04:06:44 +00:00
#region Keyboard Layout
2025-03-23 14:22:08 +00:00
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile" ;
2025-03-22 04:06:44 +00:00
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
2025-03-23 14:22:08 +00:00
private const string EnglishLanguageTag = "en" ;
2025-03-22 04:06:44 +00:00
2025-03-23 14:22:08 +00:00
private static readonly string [ ] ImeLanguageTags =
2025-03-22 11:08:09 +00:00
{
2025-03-23 14:22:08 +00:00
"zh" , // Chinese
"ja" , // Japanese
"ko" , // Korean
2025-03-22 11:08:09 +00:00
} ;
private const uint KeyboardLayoutLoWord = 0xFFFF ;
2025-03-22 04:06:44 +00:00
// Store the previous keyboard layout
private static HKL _previousLayout ;
2025-03-23 12:40:41 +00:00
/// <summary>
/// Switches the keyboard layout to English if available.
/// </summary>
/// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param>
/// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception>
2025-03-22 08:58:42 +00:00
public static unsafe void SwitchToEnglishKeyboardLayout ( bool backupPrevious )
2025-03-22 04:06:44 +00:00
{
// Find an installed English layout
var enHKL = FindEnglishKeyboardLayout ( ) ;
// No installed English layout found
if ( enHKL = = HKL . Null ) return ;
2025-04-15 01:06:31 +00:00
// Get the foreground window
var hwnd = PInvoke . GetForegroundWindow ( ) ;
2025-03-23 12:57:25 +00:00
if ( hwnd = = HWND . Null ) return ;
// Get the current foreground window thread ID
var threadId = PInvoke . GetWindowThreadProcessId ( hwnd ) ;
2025-03-22 05:58:53 +00:00
if ( threadId = = 0 ) throw new Win32Exception ( Marshal . GetLastWin32Error ( ) ) ;
2025-03-22 04:06:44 +00:00
2025-03-23 12:28:19 +00:00
// If the current layout has an IME mode, disable it without switching to another layout.
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
// the IME mode instead of switching to another layout.
2025-03-22 11:08:09 +00:00
var currentLayout = PInvoke . GetKeyboardLayout ( threadId ) ;
2025-03-23 14:22:08 +00:00
var currentLangId = ( uint ) currentLayout . Value & KeyboardLayoutLoWord ;
2025-03-24 00:51:24 +00:00
foreach ( var imeLangTag in ImeLanguageTags )
2025-03-23 14:22:08 +00:00
{
2025-03-24 00:51:24 +00:00
var langTag = GetLanguageTag ( currentLangId ) ;
if ( langTag . StartsWith ( imeLangTag , StringComparison . OrdinalIgnoreCase ) ) return ;
2025-03-23 14:22:08 +00:00
}
2025-03-22 11:08:09 +00:00
2025-03-22 04:06:44 +00:00
// Backup current keyboard layout
2025-03-23 13:05:14 +00:00
if ( backupPrevious ) _previousLayout = currentLayout ;
2025-03-22 04:06:44 +00:00
// Switch to English layout
PInvoke . ActivateKeyboardLayout ( enHKL , 0 ) ;
}
2025-03-23 12:40:41 +00:00
/// <summary>
/// Restores the previously backed-up keyboard layout.
/// If it wasn't backed up or has already been restored, this method does nothing.
/// </summary>
2025-07-19 12:26:33 +00:00
public unsafe static void RestorePreviousKeyboardLayout ( )
2025-03-22 04:06:44 +00:00
{
2025-03-22 08:58:42 +00:00
if ( _previousLayout = = HKL . Null ) return ;
2025-03-22 04:06:44 +00:00
2025-03-23 12:26:11 +00:00
var hwnd = PInvoke . GetForegroundWindow ( ) ;
2025-03-23 12:40:41 +00:00
if ( hwnd = = HWND . Null ) return ;
2025-03-23 12:26:11 +00:00
PInvoke . PostMessage (
hwnd ,
2025-03-23 11:56:52 +00:00
PInvoke . WM_INPUTLANGCHANGEREQUEST ,
PInvoke . INPUTLANGCHANGE_FORWARD ,
2025-07-19 12:26:33 +00:00
new LPARAM ( ( nint ) _previousLayout . Value )
2025-03-23 11:56:52 +00:00
) ;
2025-03-23 13:05:14 +00:00
2025-03-22 08:58:42 +00:00
_previousLayout = HKL . Null ;
2025-03-22 04:06:44 +00:00
}
2025-03-23 14:22:08 +00:00
/// <summary>
/// Finds an installed English keyboard layout.
/// </summary>
/// <returns></returns>
/// <exception cref="Win32Exception"></exception>
private static unsafe HKL FindEnglishKeyboardLayout ( )
{
// Get the number of keyboard layouts
int count = PInvoke . GetKeyboardLayoutList ( 0 , null ) ;
if ( count < = 0 ) return HKL . Null ;
// Get all keyboard layouts
var handles = new HKL [ count ] ;
fixed ( HKL * h = handles )
{
var result = PInvoke . GetKeyboardLayoutList ( count , h ) ;
if ( result = = 0 ) throw new Win32Exception ( Marshal . GetLastWin32Error ( ) ) ;
}
// Look for any English keyboard layout
foreach ( var hkl in handles )
{
// The lower word contains the language identifier
var langId = ( uint ) hkl . Value & KeyboardLayoutLoWord ;
var langTag = GetLanguageTag ( langId ) ;
// Check if it's an English layout
if ( langTag . StartsWith ( EnglishLanguageTag , StringComparison . OrdinalIgnoreCase ) )
{
return hkl ;
}
}
return HKL . Null ;
}
/// <summary>
/// Returns the
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
/// BCP 47 language tag
/// </see>
/// of the current input language.
/// </summary>
/// <remarks>
/// Edited from: https://github.com/dotnet/winforms
/// </remarks>
private static string GetLanguageTag ( uint langId )
{
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
// transient value.
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
//
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
//
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
//
// NOTE: this logic may break in future versions of Windows since it is not documented.
2025-03-23 14:41:28 +00:00
if ( langId is PInvoke . LOCALE_TRANSIENT_KEYBOARD1
or PInvoke . LOCALE_TRANSIENT_KEYBOARD2
or PInvoke . LOCALE_TRANSIENT_KEYBOARD3
or PInvoke . LOCALE_TRANSIENT_KEYBOARD4 )
2025-03-23 14:22:08 +00:00
{
2025-03-23 14:41:28 +00:00
using var key = Registry . CurrentUser . OpenSubKey ( UserProfileRegistryPath ) ;
if ( key ? . GetValue ( "Languages" ) is string [ ] languages )
2025-03-23 14:22:08 +00:00
{
foreach ( string language in languages )
{
2025-03-23 14:41:28 +00:00
using var subKey = key . OpenSubKey ( language ) ;
if ( subKey ? . GetValue ( "TransientLangId" ) is int transientLangId
2025-03-23 14:22:08 +00:00
& & transientLangId = = langId )
{
return language ;
}
}
}
}
return CultureInfo . GetCultureInfo ( ( int ) langId ) . Name ;
}
2025-03-22 04:06:44 +00:00
#endregion
2025-04-03 14:13:12 +00:00
2025-04-04 03:17:30 +00:00
#region Notification
2025-04-04 03:18:57 +00:00
2025-04-04 03:27:10 +00:00
public static bool IsNotificationSupported ( )
2025-04-03 14:13:12 +00:00
{
2025-04-04 03:27:10 +00:00
// Notifications only supported on Windows 10 19041+
2025-04-03 14:13:12 +00:00
return RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) & &
Environment . OSVersion . Version . Build > = 19041 ;
}
#endregion
2025-04-11 03:08:28 +00:00
#region Korean IME
2025-04-11 04:36:19 +00:00
public static bool IsWindows11 ( )
{
return RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) & &
Environment . OSVersion . Version . Build > = 22000 ;
}
2025-04-11 03:08:28 +00:00
public static bool IsKoreanIMEExist ( )
{
return GetLegacyKoreanIMERegistryValue ( ) ! = null ;
}
public static bool IsLegacyKoreanIMEEnabled ( )
{
object value = GetLegacyKoreanIMERegistryValue ( ) ;
if ( value is int intValue )
{
return intValue = = 1 ;
}
else if ( value ! = null & & int . TryParse ( value . ToString ( ) , out int parsedValue ) )
{
return parsedValue = = 1 ;
}
return false ;
}
public static bool SetLegacyKoreanIMEEnabled ( bool enable )
{
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}" ;
const string valueName = "NoTsf3Override5" ;
try
{
using RegistryKey key = Registry . CurrentUser . CreateSubKey ( subKeyPath ) ;
if ( key ! = null )
{
int value = enable ? 1 : 0 ;
key . SetValue ( valueName , value , RegistryValueKind . DWord ) ;
return true ;
}
}
catch ( System . Exception )
{
// Ignored
}
return false ;
}
public static object GetLegacyKoreanIMERegistryValue ( )
{
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}" ;
const string valueName = "NoTsf3Override5" ;
try
{
using RegistryKey key = Registry . CurrentUser . OpenSubKey ( subKeyPath ) ;
if ( key ! = null )
{
return key . GetValue ( valueName ) ;
}
}
catch ( System . Exception )
{
// Ignored
}
return null ;
}
public static void OpenImeSettings ( )
{
try
{
Process . Start ( new ProcessStartInfo ( "ms-settings:regionlanguage" ) { UseShellExecute = true } ) ;
}
catch ( System . Exception )
{
// Ignored
}
}
#endregion
2025-04-17 23:39:02 +00:00
#region System Font
private static readonly Dictionary < string , string > _languageToNotoSans = new ( )
{
{ "ko" , "Noto Sans KR" } ,
{ "ja" , "Noto Sans JP" } ,
{ "zh-CN" , "Noto Sans SC" } ,
{ "zh-SG" , "Noto Sans SC" } ,
{ "zh-Hans" , "Noto Sans SC" } ,
{ "zh-TW" , "Noto Sans TC" } ,
{ "zh-HK" , "Noto Sans TC" } ,
{ "zh-MO" , "Noto Sans TC" } ,
{ "zh-Hant" , "Noto Sans TC" } ,
{ "th" , "Noto Sans Thai" } ,
{ "ar" , "Noto Sans Arabic" } ,
{ "he" , "Noto Sans Hebrew" } ,
{ "hi" , "Noto Sans Devanagari" } ,
{ "bn" , "Noto Sans Bengali" } ,
{ "ta" , "Noto Sans Tamil" } ,
{ "el" , "Noto Sans Greek" } ,
{ "ru" , "Noto Sans" } ,
{ "en" , "Noto Sans" } ,
{ "fr" , "Noto Sans" } ,
{ "de" , "Noto Sans" } ,
{ "es" , "Noto Sans" } ,
{ "pt" , "Noto Sans" }
} ;
2025-04-19 03:20:08 +00:00
/// <summary>
/// Gets the system default font.
/// </summary>
/// <param name="useNoto">
/// If true, it will try to find the Noto font for the current culture.
/// </param>
/// <returns>
/// The name of the system default font.
/// </returns>
public static string GetSystemDefaultFont ( bool useNoto = true )
2025-04-17 23:39:02 +00:00
{
try
{
2025-04-19 03:20:08 +00:00
if ( useNoto )
2025-04-17 23:39:02 +00:00
{
2025-04-19 03:20:08 +00:00
var culture = CultureInfo . CurrentCulture ;
var language = culture . Name ; // e.g., "zh-TW"
var langPrefix = language . Split ( '-' ) [ 0 ] ; // e.g., "zh"
// First, try to find by full name, and if not found, fallback to prefix
if ( TryGetNotoFont ( language , out var notoFont ) | | TryGetNotoFont ( langPrefix , out notoFont ) )
2025-04-17 23:39:02 +00:00
{
2025-04-19 03:20:08 +00:00
// If the font is installed, return it
if ( Fonts . SystemFontFamilies . Any ( f = > f . Source . Equals ( notoFont ) ) )
{
return notoFont ;
}
2025-04-17 23:39:02 +00:00
}
}
// If Noto font is not found, fallback to the system default font
var font = SystemFonts . MessageFontFamily ;
if ( font . FamilyNames . TryGetValue ( XmlLanguage . GetLanguage ( "en-US" ) , out var englishName ) )
{
return englishName ;
}
return font . Source ? ? "Segoe UI" ;
}
catch
{
return "Segoe UI" ;
}
}
private static bool TryGetNotoFont ( string langKey , out string notoFont )
{
return _languageToNotoSans . TryGetValue ( langKey , out notoFont ) ;
}
#endregion
2025-05-27 06:36:09 +00:00
2025-07-20 11:11:09 +00:00
#region Window Rect
public static unsafe bool GetWindowRect ( nint handle , out Rect outRect )
{
var rect = new RECT ( ) ;
var result = PInvoke . GetWindowRect ( new ( handle ) , & rect ) ;
if ( ! result )
{
outRect = new Rect ( ) ;
return false ;
}
// Convert RECT to Rect
outRect = new Rect (
rect . left ,
rect . top ,
rect . right - rect . left ,
rect . bottom - rect . top
) ;
return true ;
}
#endregion
#region Window Process
internal static unsafe string GetProcessNameFromHwnd ( HWND hWnd )
{
return Path . GetFileName ( GetProcessPathFromHwnd ( hWnd ) ) ;
}
internal static unsafe string GetProcessPathFromHwnd ( HWND hWnd )
{
uint pid ;
var threadId = PInvoke . GetWindowThreadProcessId ( hWnd , & pid ) ;
if ( threadId = = 0 ) return string . Empty ;
var process = PInvoke . OpenProcess ( PROCESS_ACCESS_RIGHTS . PROCESS_QUERY_LIMITED_INFORMATION , false , pid ) ;
2025-07-20 11:58:49 +00:00
if ( process ! = HWND . Null )
2025-07-20 11:11:09 +00:00
{
2025-07-20 11:58:49 +00:00
using var safeHandle = new SafeProcessHandle ( ( nint ) process . Value , true ) ;
2025-07-20 11:11:09 +00:00
uint capacity = 2000 ;
Span < char > buffer = new char [ capacity ] ;
2025-07-20 11:58:49 +00:00
if ( ! PInvoke . QueryFullProcessImageName ( safeHandle , PROCESS_NAME_FORMAT . PROCESS_NAME_WIN32 , buffer , ref capacity ) )
2025-07-20 11:11:09 +00:00
{
2025-07-20 11:58:49 +00:00
return string . Empty ;
2025-07-20 11:11:09 +00:00
}
2025-07-20 11:58:49 +00:00
return buffer [ . . ( int ) capacity ] . ToString ( ) ;
2025-07-20 11:11:09 +00:00
}
return string . Empty ;
}
#endregion
2025-05-27 06:36:09 +00:00
#region Explorer
2025-05-27 06:36:24 +00:00
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems
2025-05-27 06:36:09 +00:00
public static unsafe void OpenFolderAndSelectFile ( string filePath )
{
ITEMIDLIST * pidlFolder = null ;
ITEMIDLIST * pidlFile = null ;
var folderPath = Path . GetDirectoryName ( filePath ) ;
try
{
var hrFolder = PInvoke . SHParseDisplayName ( folderPath , null , out pidlFolder , 0 , null ) ;
if ( hrFolder . Failed ) throw new COMException ( "Failed to parse folder path" , hrFolder ) ;
var hrFile = PInvoke . SHParseDisplayName ( filePath , null , out pidlFile , 0 , null ) ;
if ( hrFile . Failed ) throw new COMException ( "Failed to parse file path" , hrFile ) ;
var hrSelect = PInvoke . SHOpenFolderAndSelectItems ( pidlFolder , 1 , & pidlFile , 0 ) ;
if ( hrSelect . Failed ) throw new COMException ( "Failed to open folder and select item" , hrSelect ) ;
}
finally
{
if ( pidlFile ! = null ) PInvoke . CoTaskMemFree ( pidlFile ) ;
if ( pidlFolder ! = null ) PInvoke . CoTaskMemFree ( pidlFolder ) ;
}
}
#endregion
2025-07-05 15:05:49 +00:00
#region Win32 Dark Mode
/ *
* Inspired by https : //github.com/ysc3839/win32-darkmode
* /
[DllImport("uxtheme.dll", EntryPoint = "#135", SetLastError = true)]
private static extern int SetPreferredAppMode ( int appMode ) ;
2025-07-06 01:08:20 +00:00
public static void EnableWin32DarkMode ( string colorScheme )
2025-07-05 15:05:49 +00:00
{
try
{
2025-07-06 01:08:20 +00:00
// Undocumented API from Windows 10 1809
2025-07-05 15:05:49 +00:00
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) & &
Environment . OSVersion . Version . Build > = 17763 )
{
2025-07-06 01:08:20 +00:00
var flag = colorScheme switch
{
Constant . Light = > 3 , // ForceLight
Constant . Dark = > 2 , // ForceDark
Constant . System = > 1 , // AllowDark
_ = > 0 // Default
} ;
_ = SetPreferredAppMode ( flag ) ;
2025-07-05 15:05:49 +00:00
}
}
catch
{
// Ignore errors on unsupported OS
}
}
#endregion
2025-09-18 03:58:38 +00:00
#region File / Folder Dialog
public static string SelectFile ( )
{
var dlg = new OpenFileDialog ( ) ;
var result = dlg . ShowDialog ( ) ;
if ( result = = true )
return dlg . FileName ;
return string . Empty ;
}
#endregion
2025-09-30 13:11:03 +00:00
#region Sleep Mode Listener
private static Action _func ;
private static PDEVICE_NOTIFY_CALLBACK_ROUTINE _callback = null ;
private static DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS _recipient ;
private static SafeHandle _recipientHandle ;
private static HPOWERNOTIFY _handle = HPOWERNOTIFY . Null ;
/// <summary>
/// Registers a listener for sleep mode events.
/// Inspired from: https://github.com/XKaguya/LenovoLegionToolkit
/// https://blog.csdn.net/mochounv/article/details/114668594
/// </summary>
/// <param name="func"></param>
/// <exception cref="Win32Exception"></exception>
public static unsafe void RegisterSleepModeListener ( Action func )
{
if ( _callback ! = null )
{
// Only register if not already registered
return ;
}
_func = func ;
_callback = new PDEVICE_NOTIFY_CALLBACK_ROUTINE ( DeviceNotifyCallback ) ;
_recipient = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS ( )
{
Callback = _callback ,
Context = null
} ;
_recipientHandle = new StructSafeHandle < DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS > ( _recipient ) ;
_handle = PInvoke . PowerRegisterSuspendResumeNotification (
REGISTER_NOTIFICATION_FLAGS . DEVICE_NOTIFY_CALLBACK ,
_recipientHandle ,
out var handle ) = = WIN32_ERROR . ERROR_SUCCESS ?
new HPOWERNOTIFY ( new IntPtr ( handle ) ) :
HPOWERNOTIFY . Null ;
if ( _handle . IsNull )
{
throw new Win32Exception ( "Error registering for power notifications: " + Marshal . GetLastWin32Error ( ) ) ;
}
}
/// <summary>
/// Unregisters the sleep mode listener.
/// </summary>
public static void UnregisterSleepModeListener ( )
{
if ( ! _handle . IsNull )
{
PInvoke . PowerUnregisterSuspendResumeNotification ( _handle ) ;
_handle = HPOWERNOTIFY . Null ;
_func = null ;
_callback = null ;
_recipientHandle = null ;
}
}
private static unsafe uint DeviceNotifyCallback ( void * context , uint type , void * setting )
{
switch ( type )
{
case PInvoke . PBT_APMRESUMEAUTOMATIC :
// Operation is resuming automatically from a low-power state.This message is sent every time the system resumes
2025-09-30 13:20:00 +00:00
_func ? . Invoke ( ) ;
2025-09-30 13:11:03 +00:00
break ;
case PInvoke . PBT_APMRESUMESUSPEND :
// Operation is resuming from a low-power state.This message is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered by user input, such as pressing a key
2025-09-30 13:20:00 +00:00
_func ? . Invoke ( ) ;
2025-09-30 13:11:03 +00:00
break ;
}
return 0 ;
}
private sealed class StructSafeHandle < T > : SafeHandle where T : struct
{
private readonly nint _ptr = nint . Zero ;
public StructSafeHandle ( T recipient ) : base ( nint . Zero , true )
{
var pRecipient = Marshal . AllocHGlobal ( Marshal . SizeOf < T > ( ) ) ;
Marshal . StructureToPtr ( recipient , pRecipient , false ) ;
SetHandle ( pRecipient ) ;
_ptr = pRecipient ;
}
public override bool IsInvalid = > handle = = nint . Zero ;
protected override bool ReleaseHandle ( )
{
Marshal . FreeHGlobal ( _ptr ) ;
return true ;
}
}
#endregion
2025-12-31 10:05:53 +00:00
#region Taskbar
public static unsafe void ShowTaskbar ( )
{
// Find the taskbar window
var taskbarHwnd = PInvoke . FindWindowEx ( HWND . Null , HWND . Null , "Shell_TrayWnd" , null ) ;
if ( taskbarHwnd = = HWND . Null ) return ;
// Magic from https://github.com/Oliviaophia/SmartTaskbar
const uint TrayBarFlag = 0x05D1 ;
var mon = PInvoke . MonitorFromWindow ( taskbarHwnd , Windows . Win32 . Graphics . Gdi . MONITOR_FROM_FLAGS . MONITOR_DEFAULTTONEAREST ) ;
PInvoke . PostMessage ( taskbarHwnd , TrayBarFlag , new WPARAM ( 1 ) , new LPARAM ( ( nint ) mon . Value ) ) ;
}
public static void HideTaskbar ( )
{
// Find the taskbar window
var taskbarHwnd = PInvoke . FindWindowEx ( HWND . Null , HWND . Null , "Shell_TrayWnd" , null ) ;
if ( taskbarHwnd = = HWND . Null ) return ;
// Magic from https://github.com/Oliviaophia/SmartTaskbar
const uint TrayBarFlag = 0x05D1 ;
PInvoke . PostMessage ( taskbarHwnd , TrayBarFlag , new WPARAM ( 0 ) , IntPtr . Zero ) ;
}
#endregion
2024-12-19 08:34:32 +00:00
}
}