Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions FluentFlyoutWPF/Classes/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion FluentFlyoutWPF/Classes/WindowBlurHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
};
}
Expand Down
31 changes: 23 additions & 8 deletions FluentFlyoutWPF/Classes/WindowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NativeMethods.WINDOWPLACEMENT>() };

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
Expand All @@ -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) =>
Expand Down
60 changes: 55 additions & 5 deletions FluentFlyoutWPF/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<bool> WaitForExplorerReadyAsync(int timeoutMs = 60000)
{
var sw = Stopwatch.StartNew();
Expand Down
57 changes: 55 additions & 2 deletions FluentFlyoutWPF/Pages/LockKeysPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,24">
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,24">
<Border CornerRadius="10" ClipToBounds="True" Width="164" Height="98">
<Border.Background>
<ImageBrush ImageSource="/Resources/FluentFlyoutDemo5.1.png" Stretch="UniformToFill" />
Expand Down Expand Up @@ -98,7 +104,7 @@
<ComboBoxItem Content="{DynamicResource MonitorWithCursor}" />
</ComboBox>
</ui:CardControl>
<ui:CardControl Grid.Row="7" Icon="{ui:SymbolIcon Info24}" Margin="0,0,0,76">
<ui:CardControl Grid.Row="7" Icon="{ui:SymbolIcon Info24}" Margin="0,0,0,3">
<ui:CardControl.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{DynamicResource EnableInsertKeyTitle}" FontSize="14" FontWeight="Regular" VerticalAlignment="Center" />
Expand All @@ -107,6 +113,53 @@
</ui:CardControl.Header>
<controls:ToggleSwitch Name="LockKeysEnableInsertSwitch" IsChecked="{Binding LockKeysInsertEnabled, Mode=TwoWay}" />
</ui:CardControl>

<ui:CardControl Grid.Row="8" Icon="{ui:SymbolIcon LocalLanguage24}" Margin="0,24,0,3">
<ui:CardControl.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{DynamicResource LanguageFlyoutTitle}" FontSize="14" FontWeight="Regular" VerticalAlignment="Center" />
<TextBlock Text="{DynamicResource LanguageFlyoutDescription}" FontSize="12" Opacity="0.5" />
</StackPanel>
</ui:CardControl.Header>
<controls:ToggleSwitch Name="LanguageFlyoutSwitch" IsChecked="{Binding LanguageFlyoutEnabled, Mode=TwoWay}" />
</ui:CardControl>

<ui:CardControl Grid.Row="9" Icon="{ui:SymbolIcon Info24}" Margin="0,0,0,3">
<ui:CardControl.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{DynamicResource LanguageFlyoutShowRegionTitle}" FontSize="14" FontWeight="Regular" VerticalAlignment="Center" />
<TextBlock Text="{DynamicResource LanguageFlyoutShowRegionDescription}" FontSize="12" Opacity="0.5" />
</StackPanel>
</ui:CardControl.Header>
<controls:ToggleSwitch Name="LanguageFlyoutShowRegionSwitch" IsChecked="{Binding LanguageFlyoutShowRegion, Mode=TwoWay}" />
</ui:CardControl>

<ui:CardControl Grid.Row="10" Icon="{ui:SymbolIcon Color24}" Margin="0,0,0,3">
<ui:CardControl.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{DynamicResource LanguageFlyoutColorModeTitle}" FontSize="14" FontWeight="Regular" VerticalAlignment="Center" />
<TextBlock Text="{DynamicResource LanguageFlyoutColorModeDescription}" FontSize="12" Opacity="0.5" />
</StackPanel>
</ui:CardControl.Header>
<ComboBox SelectedIndex="{Binding LanguageFlyoutColorMode, Mode=TwoWay}" Width="150">
<ComboBoxItem Content="{DynamicResource LanguageColorModeOption1}" />
<ComboBoxItem Content="{DynamicResource LanguageColorModeOption2}" />
<ComboBoxItem Content="{DynamicResource LanguageColorModeOption3}" />
</ComboBox>
</ui:CardControl>

<ui:CardControl Grid.Row="11" Icon="{ui:SymbolIcon Timer24}" Margin="0,0,0,76">
<ui:CardControl.Header>
<StackPanel Orientation="Vertical">
<TextBlock Text="{DynamicResource LanguageFlyoutDurationTitle}" FontSize="14" FontWeight="Regular" VerticalAlignment="Center" />
<TextBlock Text="{DynamicResource LanguageFlyoutDurationDescription}" FontSize="12" Opacity="0.5" />
</StackPanel>
</ui:CardControl.Header>
<StackPanel Orientation="Horizontal">
<ui:TextBox Width="60" ClearButtonEnabled="False" Text="{Binding LanguageFlyoutDurationText, Mode=TwoWay}" />
<TextBlock Text="{DynamicResource MillisecondsUnit}" FontSize="14" FontWeight="Regular" Margin="10,0,0,0" VerticalAlignment="Center" />
</StackPanel>
</ui:CardControl>
</Grid>
</StackPanel>
</Page>
13 changes: 13 additions & 0 deletions FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ Note: As an open-source project, these features are also available for free via
<System:String x:Key="MonitorWithCursor">Monitor with cursor</System:String>
<System:String x:Key="EnableInsertKeyTitle">Enable Insert Key Popup</System:String>
<System:String x:Key="EnableInsertKeyDescription">Determines whether pressing insert/overwrite will show the flyout</System:String>
<System:String x:Key="LanguageFlyoutTitle">Enable Language flyout</System:String>
<System:String x:Key="LanguageFlyoutDescription">Show a flyout when the input language (keyboard layout) is changed</System:String>
<System:String x:Key="LanguageFlyoutAcrylicTitle">Language Flyout Acrylic</System:String>
<System:String x:Key="LanguageFlyoutAcrylicDescription">Apply transparent acrylic blur effect to the language flyout (off reverts to Mica)</System:String>
<System:String x:Key="LanguageFlyoutDurationTitle">Language flyout stay duration</System:String>
<System:String x:Key="LanguageFlyoutDurationDescription">How long the language flyout stays on the screen</System:String>
<System:String x:Key="LanguageFlyoutShowRegionTitle">Show language region</System:String>
<System:String x:Key="LanguageFlyoutShowRegionDescription">Show the country/region in parentheses (e.g. Russian (Russia))</System:String>
<System:String x:Key="LanguageFlyoutColorModeTitle">Flyout Color Mode</System:String>
<System:String x:Key="LanguageFlyoutColorModeDescription">Choose how the flyout accent color is determined</System:String>
<System:String x:Key="LanguageColorModeOption1">Automatic</System:String>
<System:String x:Key="LanguageColorModeOption2">System Accent</System:String>
<System:String x:Key="LanguageColorModeOption3">Unique per Language</System:String>
<System:String x:Key="LockKeysAnimatedTitle">Indicator Animations</System:String>
<System:String x:Key="LockKeysAnimatedDescription">Enable subtle animations for the lock key indicator</System:String>
<System:String x:Key="SystemSettingsTitle">System</System:String>
Expand Down
13 changes: 13 additions & 0 deletions FluentFlyoutWPF/Resources/Localization/Dictionary-ru.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,19 @@
<system:String x:Key="LegacyTaskbarWidthDescription">Используйте классическую систему ширины панели задач для виджетов</system:String>
<system:String x:Key="TaskbarWidgetAutoHideDescription">Автоматически скрывает виджет при остановке медиа</system:String>
<system:String x:Key="TaskbarWidgetAutoHideTitle">Автоскрытие виджета</system:String>
<system:String x:Key="LanguageFlyoutTitle">Включить уведомление о смене языка</system:String>
<system:String x:Key="LanguageFlyoutDescription">Отображать окно при смене раскладки клавиатуры</system:String>
<system:String x:Key="LanguageFlyoutAcrylicTitle">Эффект акрилового окна для языка</system:String>
<system:String x:Key="LanguageFlyoutAcrylicDescription">Применяет к окну переключения языка эффект акрилового размытия (отключение вернёт эффект Міса)</system:String>
<system:String x:Key="LanguageFlyoutDurationTitle">Длительность отображения окна языка</system:String>
<system:String x:Key="LanguageFlyoutDurationDescription">Как долго окно выбора языка остается на экране</system:String>
<system:String x:Key="LanguageFlyoutShowRegionTitle">Показывать регион языка</system:String>
<system:String x:Key="LanguageFlyoutShowRegionDescription">Отображать страну/регион в скобках (например, Русский (Россия))</system:String>
<system:String x:Key="LanguageFlyoutColorModeTitle">Цветовой режим окна</system:String>
<system:String x:Key="LanguageFlyoutColorModeDescription">Выберите, как определяется цвет акцента окна</system:String>
<system:String x:Key="LanguageColorModeOption1">Автоматически</system:String>
<system:String x:Key="LanguageColorModeOption2">Системный акцент</system:String>
<system:String x:Key="LanguageColorModeOption3">Уникальный для каждого языка</system:String>
<system:String x:Key="LockKeysAnimatedTitle">Анимация индикаторов</system:String>
<system:String x:Key="LockKeysAnimatedDescription">Включает ненавязчивую анимацию для индикатора клавиш-переключателей</system:String>
<system:String x:Key="TaskbarWidgetShowPauseOverlayTitle">Показывать значок паузы (наложение)</system:String>
Expand Down
Loading