diff --git a/GlazeWM.Bar/BarViewModel.cs b/GlazeWM.Bar/BarViewModel.cs index d3bf00df4..fd0536259 100644 --- a/GlazeWM.Bar/BarViewModel.cs +++ b/GlazeWM.Bar/BarViewModel.cs @@ -112,6 +112,7 @@ private List CreateComponentViewModels( MemoryComponentConfig rampc => new MemoryComponentViewModel(this, rampc), TextFileComponentConfig stc => new TextFileComponentViewModel(this, stc), MusicComponentConfig mcc => new MusicComponentViewModel(this, mcc), + InputLanguageComponentConfig ilcc => new InputLanguageComponentViewModel(this, ilcc), _ => throw new ArgumentOutOfRangeException(nameof(config)), }); } diff --git a/GlazeWM.Bar/Components/ComponentPortal.xaml b/GlazeWM.Bar/Components/ComponentPortal.xaml index 13d3e463a..05300a986 100644 --- a/GlazeWM.Bar/Components/ComponentPortal.xaml +++ b/GlazeWM.Bar/Components/ComponentPortal.xaml @@ -101,6 +101,11 @@ Padding="{Binding Padding}" Background="{Binding Background}" /> + + + diff --git a/GlazeWM.Bar/Components/InputLanguageComponent.xaml b/GlazeWM.Bar/Components/InputLanguageComponent.xaml new file mode 100644 index 000000000..895bb1fa1 --- /dev/null +++ b/GlazeWM.Bar/Components/InputLanguageComponent.xaml @@ -0,0 +1,10 @@ + + + diff --git a/GlazeWM.Bar/Components/InputLanguageComponent.xaml.cs b/GlazeWM.Bar/Components/InputLanguageComponent.xaml.cs new file mode 100644 index 000000000..c3545f0dd --- /dev/null +++ b/GlazeWM.Bar/Components/InputLanguageComponent.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace GlazeWM.Bar.Components +{ + /// + /// Interaction logic for InputLanguageComponent.xaml + /// + public partial class InputLanguageComponent : UserControl + { + public InputLanguageComponent() => InitializeComponent(); + } +} diff --git a/GlazeWM.Bar/Components/InputLanguageComponentViewModel.cs b/GlazeWM.Bar/Components/InputLanguageComponentViewModel.cs new file mode 100644 index 000000000..e9d0254e8 --- /dev/null +++ b/GlazeWM.Bar/Components/InputLanguageComponentViewModel.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using GlazeWM.Domain.UserConfigs; +using static GlazeWM.Infrastructure.WindowsApi.WindowsApiService; + +namespace GlazeWM.Bar.Components +{ + public class InputLanguageComponentViewModel : ComponentViewModel + { + private const uint LOCALE_ALLOW_NEUTRAL_NAMES = 0x08000000; + + private readonly InputLanguageComponentConfig _config; + + private LabelViewModel _label; + public LabelViewModel Label + { + get => _label; + protected set => SetField(ref _label, value); + } + + public InputLanguageComponentViewModel( + BarViewModel parentViewModel, + InputLanguageComponentConfig config) : base(parentViewModel, config) + { + _config = config; + + var updateInterval = TimeSpan.FromMilliseconds(_config.RefreshIntervalMs); + + Observable + .Interval(updateInterval) + .TakeUntil(_parentViewModel.WindowClosing) + .Subscribe(_ => Label = CreateLabel()); + } + + private LabelViewModel CreateLabel() + { + var variableDictionary = new Dictionary>() + { + { + "input_language", + GetInputLanguage + } + }; + + return XamlHelper.ParseLabel(_config.Label, variableDictionary, this); + } + + private static string GetInputLanguage() + { + var layout = GetKeyboardLayout(GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero)); + + // If the layout is larger than this, need different handling. + const ulong big = 0xffffffff; + + uint layoutId; + if ((ulong)layout > big) + { + layoutId = (uint)layout & 0xffff; + } + else + { + layoutId = (uint)layout >> 16; + } + + var sb = new StringBuilder(); + _ = LCIDToLocaleName(layoutId, sb, sb.Capacity, LOCALE_ALLOW_NEUTRAL_NAMES); + + return sb.ToString(); + } + } +} diff --git a/GlazeWM.Domain/UserConfigs/BarComponentConfigConverter.cs b/GlazeWM.Domain/UserConfigs/BarComponentConfigConverter.cs index c0e18f4f5..bb52170eb 100644 --- a/GlazeWM.Domain/UserConfigs/BarComponentConfigConverter.cs +++ b/GlazeWM.Domain/UserConfigs/BarComponentConfigConverter.cs @@ -101,6 +101,10 @@ public override BarComponentConfig Read( jsonObject.RootElement.ToString(), options ), + "input language" => JsonSerializer.Deserialize( + jsonObject.RootElement.ToString(), + options + ), _ => throw new ArgumentException($"Invalid component type '{typeDiscriminator}'."), }; } diff --git a/GlazeWM.Domain/UserConfigs/InputLanguageComponentConfig.cs b/GlazeWM.Domain/UserConfigs/InputLanguageComponentConfig.cs new file mode 100644 index 000000000..15b2c20a8 --- /dev/null +++ b/GlazeWM.Domain/UserConfigs/InputLanguageComponentConfig.cs @@ -0,0 +1,8 @@ +namespace GlazeWM.Domain.UserConfigs +{ + public class InputLanguageComponentConfig : BarComponentConfig + { + public string Label { get; set; } = "Input: {input_language}"; + public int RefreshIntervalMs { get; set; } = 1000; + } +} diff --git a/GlazeWM.Infrastructure/WindowsApi/WindowsApiService.cs b/GlazeWM.Infrastructure/WindowsApi/WindowsApiService.cs index 781bf002b..bd9999a85 100644 --- a/GlazeWM.Infrastructure/WindowsApi/WindowsApiService.cs +++ b/GlazeWM.Infrastructure/WindowsApi/WindowsApiService.cs @@ -364,6 +364,12 @@ public struct LowLevelKeyboardInputEvent public IntPtr AdditionalInformation; } + [DllImport("user32.dll")] + public static extern IntPtr GetKeyboardLayout(uint idThread); + + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId); + [DllImport("user32.dll")] public static extern IntPtr SetWindowsHookEx(HookType hookType, [MarshalAs(UnmanagedType.FunctionPtr)] HookProc lpfn, IntPtr hMod, int dwThreadId); @@ -448,6 +454,9 @@ public enum MonitorFromPointFlags : uint [DllImport("User32.dll")] public static extern IntPtr MonitorFromPoint(Point pt, MonitorFromPointFlags dwFlags); + [DllImport("kernel32.dll")] + public static extern int LCIDToLocaleName(uint locale, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpName, int cchName, uint dwFlags); + [DllImport("kernel32.dll")] public static extern bool GetSystemPowerStatus(out SystemPowerStatus lpSystemPowerStatus); diff --git a/README.md b/README.md index 3fbc0b966..d37c08a13 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,17 @@ Displays currently playing music. max_artist_length: 20 ``` +### Bar Component: Input language + +Displays current input language (keyboard layout). + +```yaml +- type: "input language" + label: "Input: {input_language}" + # Optional: How often this counter is refreshed. Default 1000 + refresh_interval_ms: 1000 +``` + ## Mixing font properties within a label Font family, font weight, font size, and foreground color can be changed within parts of a label. This means that icons and text fonts can be used together in a label. To customize a part of the label, wrap it in an tag: