diff --git a/FluentFlyoutWPF/Classes/NativeMethods.cs b/FluentFlyoutWPF/Classes/NativeMethods.cs index 8597c1e6..4c073900 100644 --- a/FluentFlyoutWPF/Classes/NativeMethods.cs +++ b/FluentFlyoutWPF/Classes/NativeMethods.cs @@ -253,6 +253,15 @@ internal struct WindowCompositionAttributeData [LibraryImport("user32.dll", SetLastError = true)] internal static partial uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId); + [LibraryImport("user32.dll", SetLastError = true)] + internal static partial uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [LibraryImport("user32.dll")] + internal static partial IntPtr GetKeyboardLayout(uint idThread); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + internal static extern bool GetKeyboardLayoutName([Out] StringBuilder pwszKLID); + [LibraryImport("user32.dll", EntryPoint = "SetWindowLongW")] internal static partial int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); diff --git a/FluentFlyoutWPF/Classes/WindowBlurHelper.cs b/FluentFlyoutWPF/Classes/WindowBlurHelper.cs index 3ad69e4c..f9f80cd3 100644 --- a/FluentFlyoutWPF/Classes/WindowBlurHelper.cs +++ b/FluentFlyoutWPF/Classes/WindowBlurHelper.cs @@ -1,4 +1,4 @@ -// Copyright © 2024-2026 The FluentFlyout Authors +// Copyright οΏ½ 2024-2026 The FluentFlyout Authors // SPDX-License-Identifier: GPL-3.0-or-later using FluentFlyout.Classes.Settings; @@ -141,6 +141,7 @@ private static bool ShouldHaveAcrylicBlur(Window window) "NextUpWindow" => SettingsManager.Current.NextUpAcrylicWindowEnabled, "LockWindow" => SettingsManager.Current.LockKeysAcrylicWindowEnabled, "VolumeMixerWindow" => SettingsManager.Current.VolumeMixerAcrylicWindowEnabled, + "LanguageWindow" => SettingsManager.Current.LockKeysAcrylicWindowEnabled, _ => false }; } diff --git a/FluentFlyoutWPF/Classes/WindowHelper.cs b/FluentFlyoutWPF/Classes/WindowHelper.cs index 9e6de9a2..c91b2e1b 100644 --- a/FluentFlyoutWPF/Classes/WindowHelper.cs +++ b/FluentFlyoutWPF/Classes/WindowHelper.cs @@ -39,16 +39,16 @@ public static void SetVisibility(Window window, bool visible) // workaround to s SetWindowPos(handle, 0, 0, 0, 0, 0, (uint)(SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | (visible ? SWP_SHOWWINDOW : SWP_HIDEWINDOW))); } - public static Rect GetPlacement(Window window) // get the window position, ignoring WPF + public static Rect GetPlacement(Window window) // get the window position in screen coordinates, ignoring WPF { - var wp = new NativeMethods.WINDOWPLACEMENT { length = Marshal.SizeOf() }; - var handle = new WindowInteropHelper(window).Handle; - GetWindowPlacement(handle, ref wp); - - return new Rect(wp.rcNormalPosition.Left, wp.rcNormalPosition.Top, - wp.rcNormalPosition.Right - wp.rcNormalPosition.Left, - wp.rcNormalPosition.Bottom - wp.rcNormalPosition.Top); + if (GetWindowRect(handle, out NativeMethods.RECT rect)) + { + return new Rect(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top); + } + + // Fallback (should not happen for valid windows) + return new Rect(window.Left, window.Top, window.Width, window.Height); } public static void SetPosition(Window window, double x, double y, bool async = false) // set the position of the window, ignoring WPF @@ -66,6 +66,21 @@ public static void SetPosition(Window window, double x, double y, bool async = f return; } + public static void SetPositionAndSize(Window window, double x, double y, double width, double height, bool async = false) // set the position and size of the window, ignoring WPF + { + var handle = new WindowInteropHelper(window).Handle; + uint flags = SWP_NOZORDER | (async ? SWP_ASYNCWINDOWPOS : (uint)0); + bool result = SetWindowPos(handle, 0, (int)x, (int)y, (int)width, (int)height, flags); + + if (!result) + { + int error = Marshal.GetLastWin32Error(); + Logger.Warn($"SetPositionAndSize failed for '{window.GetType().Name}' (HWND=0x{handle.ToInt64():X}, X={x}, Y={y}, W={width}, H={height}, Flags=0x{flags:X}), Win32Error={error}"); + } + + return; + } + public static void SetNoActivate(Window window) // prevent window from stealing focus { window.SourceInitialized += (sender, e) => diff --git a/FluentFlyoutWPF/MainWindow.xaml.cs b/FluentFlyoutWPF/MainWindow.xaml.cs index 64a37177..548b4a5f 100644 --- a/FluentFlyoutWPF/MainWindow.xaml.cs +++ b/FluentFlyoutWPF/MainWindow.xaml.cs @@ -70,6 +70,9 @@ public partial class MainWindow : MicaWindow private bool _isHiding = true; private LockWindow? lockWindow; + private LanguageWindow? languageWindow; + private IntPtr _lastLanguageLayout = IntPtr.Zero; + private DispatcherTimer? _languagePollingTimer; private DateTime _lastSelfUpdateTimestamp = DateTime.MinValue; internal TaskbarWindow? taskbarWindow; @@ -172,6 +175,14 @@ public MainWindow() RegisterShellHookWindow(new WindowInteropHelper(this).Handle); _positionTimer = new Timer(SeekbarUpdateUi, null, Timeout.Infinite, Timeout.Infinite); + + _languagePollingTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(75) + }; + _languagePollingTimer.Tick += LanguagePollingTimer_Tick; + _languagePollingTimer.Start(); + if (_seekBarEnabled && mediaManager.GetFocusedSession() is { } session) { UpdateSeekbarCurrentDuration(session.ControlSession.GetTimelineProperties().Position); @@ -314,11 +325,18 @@ public void OpenAnimation(MicaWindow window, bool alwaysBottom = false, MonitorI var workArea = monitor.workArea; // prevent flickering - WindowHelper.SetVisibility(window, false); // window.Visibility = Visibility.Hidden works with some delay + bool alreadyVisible = window.IsVisible; + if (!alreadyVisible) WindowHelper.SetVisibility(window, false); - // Update the DPI by moving the window to the target workArea, ignoring WPF scaling - WindowHelper.SetPosition(window, workArea.Left, workArea.Top); - var windowRect = WindowHelper.GetPlacement(window); // here we take the updated window size in raw coordinates. + window.UpdateLayout(); // Ensure layout is computed before querying size + + // Calculate the window size in raw pixels manually to avoid the "jump to top-left". + // We use the monitor's DPI directly to scale WPF units. + double currentWidth = double.IsNaN(window.Width) ? window.ActualWidth : window.Width; + double currentHeight = double.IsNaN(window.Height) ? window.ActualHeight : window.Height; + double rawWidth = Math.Ceiling(currentWidth * monitor.dpiX / 96.0); + double rawHeight = Math.Ceiling(currentHeight * monitor.dpiY / 96.0); + var windowRect = new Rect(0, 0, rawWidth, rawHeight); double window_left = 0; @@ -417,7 +435,8 @@ public void OpenAnimation(MicaWindow window, bool alwaysBottom = false, MonitorI } // Set the initial position in raw coordinates. - WindowHelper.SetPosition(window, window_left, moveAnimation.From!.Value); + if (moveAnimation.From != null) + WindowHelper.SetPosition(window, window_left, moveAnimation.From.Value); // Next coordinates will be used to set Window.Top, which takes DPI into account, // so we need to convert the coordinates to DPI scale. @@ -762,6 +781,13 @@ private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) lockWindow.ShowLockFlyout("Insert", Keyboard.IsKeyToggled(Key.Insert)); } } + + // Trigger Language Flyout on Win + Space instantly + if (vkCode == 0x20 && (Keyboard.Modifiers & ModifierKeys.Windows) != 0 && wParam == WM_KEYDOWN) + { + languageWindow ??= new LanguageWindow(); + languageWindow.ShowLanguageFlyout(); + } } return CallNextHookEx(_hookId, nCode, wParam, lParam); } @@ -1401,6 +1427,30 @@ private void NotifyIconQuit_Click(object sender, RoutedEventArgs e) } } + private void LanguagePollingTimer_Tick(object? sender, EventArgs e) + { + if (!SettingsManager.Current.LanguageFlyoutEnabled) return; + + IntPtr foregroundWindow = NativeMethods.GetForegroundWindow(); + if (foregroundWindow == IntPtr.Zero) return; + + uint threadId = NativeMethods.GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero); + IntPtr hkl = NativeMethods.GetKeyboardLayout(threadId); + + if (_lastLanguageLayout == IntPtr.Zero) + { + _lastLanguageLayout = hkl; + return; + } + + if (hkl != _lastLanguageLayout) + { + _lastLanguageLayout = hkl; + languageWindow ??= new LanguageWindow(); + languageWindow.ShowLanguageFlyout(); + } + } + private async Task WaitForExplorerReadyAsync(int timeoutMs = 60000) { var sw = Stopwatch.StartNew(); diff --git a/FluentFlyoutWPF/Pages/LockKeysPage.xaml b/FluentFlyoutWPF/Pages/LockKeysPage.xaml index 1cb689d5..438e01b3 100644 --- a/FluentFlyoutWPF/Pages/LockKeysPage.xaml +++ b/FluentFlyoutWPF/Pages/LockKeysPage.xaml @@ -25,8 +25,14 @@ + + + + + + - + @@ -98,7 +104,7 @@ - + @@ -107,6 +113,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml b/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml index 2a43079a..25470631 100644 --- a/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml +++ b/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml @@ -131,6 +131,19 @@ Note: As an open-source project, these features are also available for free via Monitor with cursor Enable Insert Key Popup Determines whether pressing insert/overwrite will show the flyout + Enable Language flyout + Show a flyout when the input language (keyboard layout) is changed + Language Flyout Acrylic + Apply transparent acrylic blur effect to the language flyout (off reverts to Mica) + Language flyout stay duration + How long the language flyout stays on the screen + Show language region + Show the country/region in parentheses (e.g. Russian (Russia)) + Flyout Color Mode + Choose how the flyout accent color is determined + Automatic + System Accent + Unique per Language Indicator Animations Enable subtle animations for the lock key indicator System diff --git a/FluentFlyoutWPF/Resources/Localization/Dictionary-ru.xaml b/FluentFlyoutWPF/Resources/Localization/Dictionary-ru.xaml index 49f17267..a9be34f7 100644 --- a/FluentFlyoutWPF/Resources/Localization/Dictionary-ru.xaml +++ b/FluentFlyoutWPF/Resources/Localization/Dictionary-ru.xaml @@ -231,6 +231,19 @@ Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ ΠΊΠ»Π°ΡΡΠΈΡ‡Π΅ΡΠΊΡƒΡŽ систСму ΡˆΠΈΡ€ΠΈΠ½Ρ‹ ΠΏΠ°Π½Π΅Π»ΠΈ Π·Π°Π΄Π°Ρ‡ для Π²ΠΈΠ΄ΠΆΠ΅Ρ‚ΠΎΠ² АвтоматичСски скрываСт Π²ΠΈΠ΄ΠΆΠ΅Ρ‚ ΠΏΡ€ΠΈ остановкС ΠΌΠ΅Π΄ΠΈΠ° АвтоскрытиС Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Π° + Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΎ смСнС языка + ΠžΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Ρ‚ΡŒ ΠΎΠΊΠ½ΠΎ ΠΏΡ€ΠΈ смСнС раскладки ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρ‹ + Π­Ρ„Ρ„Π΅ΠΊΡ‚ Π°ΠΊΡ€ΠΈΠ»ΠΎΠ²ΠΎΠ³ΠΎ ΠΎΠΊΠ½Π° для языка + ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅Ρ‚ ΠΊ ΠΎΠΊΠ½Ρƒ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ языка эффСкт Π°ΠΊΡ€ΠΈΠ»ΠΎΠ²ΠΎΠ³ΠΎ размытия (ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Π²Π΅Ρ€Π½Ρ‘Ρ‚ эффСкт ΠœΡ–ΡΠ°) + Π”Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ отобраТСния ΠΎΠΊΠ½Π° языка + Как Π΄ΠΎΠ»Π³ΠΎ ΠΎΠΊΠ½ΠΎ Π²Ρ‹Π±ΠΎΡ€Π° языка остаСтся Π½Π° экранС + ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Ρ€Π΅Π³ΠΈΠΎΠ½ языка + ΠžΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Ρ‚ΡŒ страну/Ρ€Π΅Π³ΠΈΠΎΠ½ Π² скобках (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, Русский (Россия)) + Π¦Π²Π΅Ρ‚ΠΎΠ²ΠΎΠΉ Ρ€Π΅ΠΆΠΈΠΌ ΠΎΠΊΠ½Π° + Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅, ΠΊΠ°ΠΊ опрСдСляСтся Ρ†Π²Π΅Ρ‚ Π°ΠΊΡ†Π΅Π½Ρ‚Π° ΠΎΠΊΠ½Π° + АвтоматичСски + БистСмный Π°ΠΊΡ†Π΅Π½Ρ‚ + Π£Π½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ языка Анимация ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ‚ΠΎΡ€ΠΎΠ² Π’ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Π½Π΅Π½Π°Π²ΡΠ·Ρ‡ΠΈΠ²ΡƒΡŽ Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΡŽ для ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ‚ΠΎΡ€Π° клавиш-ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Ρ‚Π΅Π»Π΅ΠΉ ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π·Π½Π°Ρ‡ΠΎΠΊ ΠΏΠ°ΡƒΠ·Ρ‹ (Π½Π°Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅) diff --git a/FluentFlyoutWPF/ViewModels/UserSettings.cs b/FluentFlyoutWPF/ViewModels/UserSettings.cs index 27544d98..19404c50 100644 --- a/FluentFlyoutWPF/ViewModels/UserSettings.cs +++ b/FluentFlyoutWPF/ViewModels/UserSettings.cs @@ -202,6 +202,55 @@ public string LockKeysDurationText } } + /// + /// Enable language switcher flyout + /// + [ObservableProperty] + public partial bool LanguageFlyoutEnabled { get; set; } + + /// + /// Show region/country in parentheses in language flyout + /// + [ObservableProperty] + public partial bool LanguageFlyoutShowRegion { get; set; } + + /// + /// Color mode for language flyout (0: Auto, 1: System, 2: Unique) + /// + [ObservableProperty] + public partial int LanguageFlyoutColorMode { get; set; } + + /// + /// Language flyout display duration (milliseconds) + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(LanguageFlyoutDurationText))] + public partial int LanguageFlyoutDuration { get; set; } + + [XmlIgnore] + public string LanguageFlyoutDurationText + { + get => LanguageFlyoutDuration.ToString(); + set + { + if (int.TryParse(value, out var result)) + { + LanguageFlyoutDuration = result switch + { + > 10000 => 10000, + < 0 => 0, + _ => result + }; + } + else + { + LanguageFlyoutDuration = 2000; + } + + OnPropertyChanged(); + } + } + /// /// App theme. 0 for default, 1 for light, 2 for dark. /// @@ -597,6 +646,8 @@ public UserSettings() FlyoutAnimationEasingStyle = 2; LockKeysEnabled = true; LockKeysDuration = 2000; + LanguageFlyoutEnabled = true; + LanguageFlyoutDuration = 2000; AppTheme = 0; MediaFlyoutEnabled = true; MediaFlyoutAlwaysDisplay = false; diff --git a/FluentFlyoutWPF/Windows/LanguageWindow.xaml b/FluentFlyoutWPF/Windows/LanguageWindow.xaml new file mode 100644 index 00000000..b20620db --- /dev/null +++ b/FluentFlyoutWPF/Windows/LanguageWindow.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FluentFlyoutWPF/Windows/LanguageWindow.xaml.cs b/FluentFlyoutWPF/Windows/LanguageWindow.xaml.cs new file mode 100644 index 00000000..fcbf531d --- /dev/null +++ b/FluentFlyoutWPF/Windows/LanguageWindow.xaml.cs @@ -0,0 +1,212 @@ +// Copyright Β© 2024-2026 The FluentFlyout Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +using FluentFlyout.Classes; +using FluentFlyout.Classes.Settings; +using FluentFlyoutWPF.Classes; +using MicaWPF.Controls; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; +using static FluentFlyoutWPF.Classes.Utils.MonitorUtil; + +namespace FluentFlyoutWPF.Windows; + +/// +/// Interaction logic for LanguageWindow.xaml +/// +public partial class LanguageWindow : MicaWindow +{ + private CancellationTokenSource cts = new(); + private MainWindow _mainWindow = (MainWindow)Application.Current.MainWindow; + private bool _isHiding = true; + private MonitorInfo _openedMonitor; + + public LanguageWindow() + { + DataContext = SettingsManager.Current; + WindowHelper.SetNoActivate(this); + InitializeComponent(); + WindowHelper.SetTopmost(this); + CustomWindowChrome.CaptionHeight = 0; + + WindowStartupLocation = WindowStartupLocation.Manual; + Top = -9999; // start off-screen + Width = 160; + Left = (SystemParameters.WorkArea.Width - Width) / 2; + } + + private Color GetShiftedColor(Color baseColor, string languageCode, string seedText) + { + int mode = SettingsManager.Current.LanguageFlyoutColorMode; + + // Mode 1: Always System Accent + if (mode == 1) return baseColor; + + // Mode 0: Auto (System for primary, Unique for secondary) + if (mode == 0) + { + string systemLangCode = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + if (languageCode.Equals(systemLangCode, StringComparison.OrdinalIgnoreCase)) return baseColor; + } + + // Mode 2 (Unique) or secondary in Mode 0 + int seed = 0; + foreach (char c in seedText) seed += (int)c; + + System.Drawing.Color drawingColor = System.Drawing.Color.FromArgb(baseColor.A, baseColor.R, baseColor.G, baseColor.B); + float hue = drawingColor.GetHue(); + float saturation = drawingColor.GetSaturation(); + float brightness = drawingColor.GetBrightness(); + + // Shift hue significantly (between 60 and 300 degrees) to be visually different + float hueShift = 60 + (Math.Abs(seed) % 240); + float newHue = (hue + hueShift) % 360; + + return ColorFromAhsb(baseColor.A, newHue, saturation, brightness); + } + + private Color ColorFromAhsb(byte a, float h, float s, float l) + { + float r, g, b; + if (s == 0) r = g = b = l; + else + { + float q = l < 0.5 ? l * (1 + s) : l + s - l * s; + float p = 2 * l - q; + r = HueToRgb(p, q, h / 360f + 1f / 3f); + g = HueToRgb(p, q, h / 360f); + b = HueToRgb(p, q, h / 360f - 1f / 3f); + } + return Color.FromArgb(a, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); + } + + private float HueToRgb(float p, float q, float t) + { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1f / 6f) return p + (q - p) * 6 * t; + if (t < 1f / 2f) return q; + if (t < 2f / 3f) return p + (q - p) * (2f / 3f - t) * 6; + return p; + } + + public async void ShowLanguageFlyout() + { + if (!SettingsManager.Current.LanguageFlyoutEnabled) return; + + await Dispatcher.InvokeAsync(() => + { + if (SettingsManager.Current.LockKeysAcrylicWindowEnabled) + { + WindowBlurHelper.EnableBlur(this); + } + else + { + WindowBlurHelper.DisableBlur(this); + } + + // Get current keyboard layout + IntPtr foregroundWindow = NativeMethods.GetForegroundWindow(); + if (foregroundWindow == IntPtr.Zero) foregroundWindow = NativeMethods.FindWindow("Shell_TrayWnd", null); + uint threadId = NativeMethods.GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero); + IntPtr hkl = NativeMethods.GetKeyboardLayout(threadId); + if (hkl == IntPtr.Zero) hkl = NativeMethods.GetKeyboardLayout(0); + + int lcid = (int)((long)hkl & 0xFFFF); + + try + { + CultureInfo culture = new CultureInfo(lcid); + string langCode = culture.TwoLetterISOLanguageName; + LangShortText.Text = langCode.ToUpper(); + + string name = culture.NativeName; + if (!SettingsManager.Current.LanguageFlyoutShowRegion) + { + int parenIndex = name.IndexOf('('); + if (parenIndex > 0) name = name.Substring(0, parenIndex).Trim(); + } + if (!string.IsNullOrEmpty(name)) name = char.ToUpper(name[0]) + name.Substring(1); + LangFullText.Text = name; + + // Apply color + object colorObj = Application.Current.TryFindResource("MicaWPF.Colors.AccentFillColorDefault"); + Color systemColor = colorObj is Color c ? c : ((SolidColorBrush)Application.Current.TryFindResource("MicaWPF.Brushes.AccentFillColorDefault")).Color; + AccentIndicator.Fill = new SolidColorBrush(GetShiftedColor(systemColor, langCode, name)); + + // Force measurement of the content to get the required width before rendering + ContentStack.Measure(new Size(double.PositiveInfinity, 50)); + double targetWidth = Math.Max(160, ContentStack.DesiredSize.Width + 60); + + var monitor = GetSelectedMonitor(SettingsManager.Current.FlyoutSelectedMonitor); + double newRawWidth = Math.Ceiling(targetWidth * monitor.dpiX / 96.0); + double newLeft = Math.Ceiling(monitor.workArea.Left + (monitor.workArea.Width / 2) - (newRawWidth / 2)); + + if (_isHiding) + { + _isHiding = false; + Width = targetWidth; + Left = newLeft * 96.0 / monitor.dpiX; + _openedMonitor = monitor; + _mainWindow.OpenAnimation(window: this, alwaysBottom: true, selectedMonitor: _openedMonitor); + } + else + { + // BEAUTIFUL TRANSITION: Animate width and center position together + DoubleAnimation widthAnim = new DoubleAnimation + { + To = targetWidth, + Duration = TimeSpan.FromMilliseconds(75), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }; + + DoubleAnimation leftAnim = new DoubleAnimation + { + To = newLeft * 96.0 / monitor.dpiX, + Duration = TimeSpan.FromMilliseconds(75), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }; + + this.BeginAnimation(Window.WidthProperty, widthAnim); + this.BeginAnimation(Window.LeftProperty, leftAnim); + } + } + catch + { + LangShortText.Text = "??"; + LangFullText.Text = "Language"; + if (_isHiding) + { + _isHiding = false; + Width = 160; + _openedMonitor = GetSelectedMonitor(SettingsManager.Current.FlyoutSelectedMonitor); + _mainWindow.OpenAnimation(window: this, alwaysBottom: true, selectedMonitor: _openedMonitor); + } + } + }); + + cts.Cancel(); + cts = new CancellationTokenSource(); + var token = cts.Token; + + try + { + while (!token.IsCancellationRequested) + { + await Task.Delay(SettingsManager.Current.LanguageFlyoutDuration, token); + _mainWindow.CloseAnimation(window: this, selectedMonitor: _openedMonitor); + _isHiding = true; + await Task.Delay(MainWindow.getDuration()); + if (_isHiding == false) return; + + WindowHelper.SetVisibility(this, false); + break; + } + } + catch (TaskCanceledException) { } + } +}