Flow.Launcher/Flow.Launcher/MainWindow.xaml.cs
Jack251970 a2f224225e Move progress bar animation init to Loaded event
InitProgressbarAnimation is now triggered by the progress bar's Loaded event instead of during main window initialization. This ensures the animation starts only after the progress bar is fully loaded, improving reliability and preventing potential UI timing issues.
2026-03-02 18:20:09 +08:00

1502 lines
56 KiB
C#

using System;
using System.ComponentModel;
using System.Linq;
using System.Media;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Shell;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern;
using iNKORE.UI.WPF.Modern.Controls;
using DataObject = System.Windows.DataObject;
using Key = System.Windows.Input.Key;
using MouseButtons = System.Windows.Forms.MouseButtons;
using NotifyIcon = System.Windows.Forms.NotifyIcon;
namespace Flow.Launcher
{
public partial class MainWindow : IDisposable
{
#region Public Property
// Window Event: Close Event
public bool CanClose { get; set; } = false;
#endregion
#region Private Fields
// Class Name
private static readonly string ClassName = nameof(MainWindow);
// Dependency Injection
private readonly Settings _settings;
private readonly Theme _theme;
// Window Notify Icon
private NotifyIcon _notifyIcon;
// Window Context Menu
private readonly ContextMenu _contextMenu = new();
private readonly MainViewModel _viewModel;
// Window Event: Key Event
private bool _isArrowKeyPressed = false;
// Window Sound Effects
private MediaPlayer _animationSoundWMP;
private SoundPlayer _animationSoundWPF;
private readonly Lock _soundLock = new();
// Window WndProc
private HwndSource _hwndSource;
private int _initialWidth;
private int _initialHeight;
// Window Animation
private const double DefaultRightMargin = 66; //* this value from base.xaml
private bool _isClockPanelAnimating = false;
// IDisposable
private bool _disposed = false;
#endregion
#region Constructor
public MainWindow()
{
_settings = Ioc.Default.GetRequiredService<Settings>();
_theme = Ioc.Default.GetRequiredService<Theme>();
_viewModel = Ioc.Default.GetRequiredService<MainViewModel>();
DataContext = _viewModel;
Topmost = _settings.ShowAtTopmost;
InitializeComponent();
UpdatePosition();
InitSoundEffects();
RegisterSoundEffectsEvent();
DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste);
_viewModel.ActualApplicationThemeChanged += ViewModel_ActualApplicationThemeChanged;
}
#endregion
#region Window Event
#pragma warning disable VSTHRD100 // Avoid async void methods
private void ViewModel_ActualApplicationThemeChanged(object sender, ActualApplicationThemeChangedEventArgs args)
{
_ = _theme.RefreshFrameAsync();
}
private void OnSourceInitialized(object sender, EventArgs e)
{
var handle = Win32Helper.GetWindowHandle(this, true);
_hwndSource = HwndSource.FromHwnd(handle);
_hwndSource.AddHook(WndProc);
Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this);
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Check first launch
if (_settings.FirstLaunch)
{
// Set First Launch to false
_settings.FirstLaunch = false;
// Update release notes version
_settings.ReleaseNotesVersion = Constant.Version;
// Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None
if (Win32Helper.IsBackdropSupported()) _settings.BackdropType = BackdropTypes.Acrylic;
// Save settings
App.API.SaveAppAllSettings();
// Show Welcome Window
var welcomeWindow = new WelcomeWindow();
welcomeWindow.Show();
}
if (Constant.Version != "1.0.0" && _settings.ReleaseNotesVersion != Constant.Version) // Skip release notes notification for developer builds (version 1.0.0)
{
// Update release notes version
_settings.ReleaseNotesVersion = Constant.Version;
// Show release note popup with button
App.API.ShowMsgWithButton(
Localize.appUpdateTitle(Constant.Version),
Localize.appUpdateButtonContent(),
() =>
{
Application.Current.Dispatcher.Invoke(() =>
{
var releaseNotesWindow = new ReleaseNotesWindow();
releaseNotesWindow.Show();
});
});
}
// Initialize place holder
SetupPlaceholderText();
_viewModel.PlaceholderText = _settings.PlaceholderText;
// Hide window if need
UpdatePosition();
if (_settings.HideOnStartup)
{
_viewModel.Hide();
_viewModel.InitializeVisibilityStatus(false);
}
else
{
_viewModel.Show();
_viewModel.InitializeVisibilityStatus(true);
// When HideOnStartup is off and UseAnimation is on,
// there was a bug where the clock would not appear at all on the initial launch
// So we need to forcibly trigger animation here to ensure the clock is visible
if (_settings.UseAnimation)
{
WindowAnimation();
}
}
// Initialize context menu & notify icon
InitializeContextMenu();
InitializeNotifyIcon();
// Initialize color scheme
if (_settings.ColorScheme == Constant.Light)
{
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light;
}
else if (_settings.ColorScheme == Constant.Dark)
{
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark;
}
// Force update position
UpdatePosition();
// Initialize resize mode after refreshing frame
SetupResizeMode();
// Reset preview
_viewModel.ResetPreview();
// Since the default main window visibility is visible, so we need set focus during startup
QueryTextBox.Focus();
// Set the initial state of the QueryTextBoxCursorMovedToEnd property
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
_viewModel.QueryTextCursorMovedToEnd = false;
// Register Dialog Jump events
InitializeDialogJump();
// View model property changed event
_viewModel.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
{
case nameof(MainViewModel.MainWindowVisibilityStatus):
{
Dispatcher.Invoke(() =>
{
if (_viewModel.MainWindowVisibilityStatus)
{
// Play sound effect before activing the window
if (_settings.UseSound && !_viewModel.IsDialogJumpWindowUnderDialog())
{
SoundPlay();
}
// Update position & Activate
UpdatePosition();
Activate();
// Reset preview
_viewModel.ResetPreview();
// Select last query if need
if (!_viewModel.LastQuerySelected)
{
QueryTextBox.SelectAll();
_viewModel.LastQuerySelected = true;
}
// Focus query box
QueryTextBox.Focus();
// Play window animation
if (_settings.UseAnimation && !_viewModel.IsDialogJumpWindowUnderDialog())
{
WindowAnimation();
}
// Update activate times
_settings.ActivateTimes++;
}
});
break;
}
case nameof(MainViewModel.QueryTextCursorMovedToEnd):
if (_viewModel.QueryTextCursorMovedToEnd)
{
// QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle.
// To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority
Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length);
_viewModel.QueryTextCursorMovedToEnd = false;
}
break;
case nameof(MainViewModel.GameModeStatus):
_notifyIcon.Icon = _viewModel.GameModeStatus
? Properties.Resources.gamemode
: Properties.Resources.app;
break;
}
};
// Settings property changed event
_settings.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
{
case nameof(Settings.HideNotifyIcon):
_notifyIcon.Visible = !_settings.HideNotifyIcon;
break;
case nameof(Settings.Language):
UpdateNotifyIconText();
if (_settings.ShowHomePage && _viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
}
break;
case nameof(Settings.Hotkey):
UpdateNotifyIconText();
break;
case nameof(Settings.WindowLeft):
Left = _settings.WindowLeft;
break;
case nameof(Settings.WindowTop):
Top = _settings.WindowTop;
break;
case nameof(Settings.ShowPlaceholder):
SetupPlaceholderText();
break;
case nameof(Settings.PlaceholderText):
_viewModel.PlaceholderText = _settings.PlaceholderText;
break;
case nameof(Settings.KeepMaxResults):
SetupResizeMode();
break;
case nameof(Settings.SettingWindowFont):
InitializeContextMenu();
break;
case nameof(Settings.ShowHomePage):
case nameof(Settings.ShowHistoryResultsForHomePage):
case nameof(Settings.HistoryStyle):
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
}
break;
case nameof(Settings.ShowAtTopmost):
Topmost = _settings.ShowAtTopmost;
break;
}
};
// QueryTextBox.Text change detection (modified to only work when character count is 1 or higher)
QueryTextBox.TextChanged += (s, e) => UpdateClockPanelVisibility();
// Detecting ResultContextMenu.Visibility changes
DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(ResultListBox))
.AddValueChanged(ResultContextMenu, (s, e) => UpdateClockPanelVisibility());
// Detect History.Visibility changes
DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(ResultListBox))
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
// Initialize query state
if (_settings.ShowHomePage && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
}
}
private void ProgressBar_Loaded(object sender, RoutedEventArgs e)
{
InitProgressbarAnimation();
}
private async void OnClosing(object sender, CancelEventArgs e)
{
if (!CanClose)
{
CanClose = true;
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
// After plugins are all disposed, we shutdown application to close app
// We use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
Application.Current.Shutdown();
}
}
private void OnClosed(object sender, EventArgs e)
{
try
{
_hwndSource.RemoveHook(WndProc);
}
catch (Exception)
{
// Ignored
}
_hwndSource = null;
}
private void OnLocationChanged(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
if (IsLoaded)
{
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
}
}
private async void OnDeactivated(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
_viewModel.ClockPanelOpacity = 0.0;
_viewModel.SearchIconOpacity = 0.0;
// This condition stops extra hide call when animator is on,
// which causes the toggling to occasional hide instead of show.
if (_viewModel.MainWindowVisibilityStatus)
{
// Need time to initialize the main query window animation.
// This also stops the mainwindow from flickering occasionally after Settings window is opened
// and always after Settings window is closed.
if (_settings.UseAnimation)
{
await Task.Delay(100);
}
if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible)
{
_viewModel.Hide();
}
}
}
private void OnKeyDown(object sender, KeyEventArgs e)
{
var specialKeyState = GlobalHotkey.CheckModifiers();
switch (e.Key)
{
case Key.Down:
_isArrowKeyPressed = true;
_viewModel.SelectNextItemCommand.Execute(null);
e.Handled = true;
break;
case Key.Up:
_isArrowKeyPressed = true;
_viewModel.SelectPrevItemCommand.Execute(null);
e.Handled = true;
break;
case Key.PageDown:
_viewModel.SelectNextPageCommand.Execute(null);
e.Handled = true;
break;
case Key.PageUp:
_viewModel.SelectPrevPageCommand.Execute(null);
e.Handled = true;
break;
case Key.Right:
if (_viewModel.QueryResultsSelected()
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length
&& !string.IsNullOrEmpty(QueryTextBox.Text))
{
_viewModel.LoadContextMenuCommand.Execute(null);
e.Handled = true;
}
break;
case Key.Left:
if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
{
_viewModel.EscCommand.Execute(null);
e.Handled = true;
}
break;
case Key.Back:
if (specialKeyState.CtrlPressed)
{
if (_viewModel.QueryResultsSelected()
&& QueryTextBox.Text.Length > 0
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{
_viewModel.BackspaceCommand.Execute(null);
e.Handled = true;
}
}
}
break;
default:
break;
}
}
private void OnKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Up || e.Key == Key.Down)
{
_isArrowKeyPressed = false;
}
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (_isArrowKeyPressed)
{
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
}
}
#pragma warning restore VSTHRD100 // Avoid async void methods
#endregion
#region Window Boarder Event
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
// When the window is maximized via Snap,
// dragging attempts will first switch the window from Maximized to Normal state,
// and adjust the drag position accordingly.
if (e.ChangedButton == MouseButton.Left)
{
try
{
if (WindowState == WindowState.Maximized)
{
// Calculate ratio based on maximized window dimensions
double maxWidth = ActualWidth;
double maxHeight = ActualHeight;
var mousePos = e.GetPosition(this);
double xRatio = mousePos.X / maxWidth;
double yRatio = mousePos.Y / maxHeight;
// Current monitor information
var screen = MonitorInfo.GetNearestDisplayMonitor(new WindowInteropHelper(this).Handle);
var workingArea = screen.WorkingArea;
var screenLeftTop = Win32Helper.TransformPixelsToDIP(this, workingArea.X, workingArea.Y);
// Switch to Normal state
WindowState = WindowState.Normal;
Application.Current?.Dispatcher.Invoke(new Action(() =>
{
double normalWidth = Width;
double normalHeight = Height;
// Apply ratio based on the difference between maximized and normal window sizes
Left = screenLeftTop.X + (maxWidth - normalWidth) * xRatio;
Top = screenLeftTop.Y + (maxHeight - normalHeight) * yRatio;
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
DragMove();
}
}), DispatcherPriority.ApplicationIdle);
}
else
{
DragMove();
}
}
catch (InvalidOperationException)
{
// Ignored - can occur if drag operation is already in progress
}
}
}
#endregion
#region Window Context Menu Event
#pragma warning disable VSTHRD100 // Avoid async void methods
private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e)
{
_viewModel.Hide();
if (_settings.UseAnimation)
await Task.Delay(100);
App.API.OpenSettingDialog();
}
#pragma warning restore VSTHRD100 // Avoid async void methods
#endregion
#region Window WndProc
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case Win32Helper.WM_ENTERSIZEMOVE:
// Do do handle size move event for dialog jump window
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return IntPtr.Zero;
}
_initialWidth = (int)Width;
_initialHeight = (int)Height;
handled = true;
break;
case Win32Helper.WM_EXITSIZEMOVE:
// Do do handle size move event for Dialog Jump window
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return IntPtr.Zero;
}
//Prevent updating the number of results when the window height is below the height of a single result item.
//This situation occurs not only when the user manually resizes the window, but also when the window is released from a side snap, as the OS automatically adjusts the window height.
//(Without this check, releasing from a snap can cause the window height to hit the minimum, resulting in only 2 results being shown.)
if (_initialHeight != (int)Height && Height > (_settings.WindowHeightSize + _settings.ItemHeightSize))
{
if (!_settings.KeepMaxResults)
{
// Get shadow margin
var shadowMargin = 0;
var (_, useDropShadowEffect) = _theme.GetActualValue();
if (useDropShadowEffect)
{
shadowMargin = 32;
}
// Calculate max results to show
var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize;
if (itemCount < 2)
{
_settings.MaxResultsToShow = 2;
}
else
{
_settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount));
}
}
SizeToContent = SizeToContent.Height;
}
else
{
// Update height when exiting maximized snap state.
SizeToContent = SizeToContent.Height;
}
if (_initialWidth != (int)Width)
{
if (!_settings.KeepMaxResults)
{
// Update width
_viewModel.MainWindowWidth = Width;
}
SizeToContent = SizeToContent.Height;
}
handled = true;
break;
case Win32Helper.WM_NCLBUTTONDBLCLK: // Block the double click in frame
SizeToContent = SizeToContent.Height;
handled = true;
break;
case Win32Helper.WM_SYSCOMMAND: // Block Maximize/Minimize by Win+Up and Win+Down Arrow
var command = wParam.ToInt32() & 0xFFF0;
if (command == Win32Helper.SC_MAXIMIZE || command == Win32Helper.SC_MINIMIZE)
{
SizeToContent = SizeToContent.Height;
handled = true;
}
break;
}
return IntPtr.Zero;
}
#endregion
#region Window Sound Effects
private void InitSoundEffects()
{
lock (_soundLock)
{
if (_settings.WMPInstalled)
{
_animationSoundWMP?.Close();
_animationSoundWMP = new MediaPlayer();
_animationSoundWMP.Open(new Uri(AppContext.BaseDirectory + "Resources\\open.wav"));
}
else
{
_animationSoundWPF?.Dispose();
_animationSoundWPF = new SoundPlayer(AppContext.BaseDirectory + "Resources\\open.wav");
_animationSoundWPF.Load();
}
}
}
private void SoundPlay()
{
lock (_soundLock)
{
if (_settings.WMPInstalled)
{
_animationSoundWMP.Position = TimeSpan.Zero;
_animationSoundWMP.Volume = _settings.SoundVolume / 100.0;
_animationSoundWMP.Play();
}
else
{
_animationSoundWPF.Play();
}
}
}
private void RegisterSoundEffectsEvent()
{
// Fix for sound not playing after sleep / hibernate for both modern standby and legacy standby
// https://stackoverflow.com/questions/64805186/mediaplayer-doesnt-play-after-computer-sleeps
try
{
Win32Helper.RegisterSleepModeListener(() =>
{
if (Application.Current == null)
{
return;
}
// We must run InitSoundEffects on UI thread because MediaPlayer is a DispatcherObject
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(InitSoundEffects);
return;
}
InitSoundEffects();
});
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to register sound effect event", e);
}
}
private static void UnregisterSoundEffectsEvent()
{
try
{
Win32Helper.UnregisterSleepModeListener();
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to unregister sound effect event", e);
}
}
#endregion
#region Window Notify Icon
private void InitializeNotifyIcon()
{
_notifyIcon = new NotifyIcon
{
Text = Constant.FlowLauncherFullName,
Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app,
Visible = !_settings.HideNotifyIcon
};
_notifyIcon.MouseClick += (o, e) =>
{
switch (e.Button)
{
case MouseButtons.Left:
_viewModel.ToggleFlowLauncher();
break;
case MouseButtons.Right:
_contextMenu.IsOpen = true;
// Get context menu handle and bring it to the foreground
if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource)
{
Win32Helper.SetForegroundWindow(hwndSource.Handle);
}
_contextMenu.Focus();
break;
}
};
}
private void UpdateNotifyIconText()
{
var menu = _contextMenu;
((MenuItem)menu.Items[0]).Header = Localize.iconTrayOpen() +
" (" + _settings.Hotkey + ")";
((MenuItem)menu.Items[1]).Header = Localize.GameMode();
((MenuItem)menu.Items[2]).Header = Localize.PositionReset();
((MenuItem)menu.Items[3]).Header = Localize.iconTraySettings();
((MenuItem)menu.Items[4]).Header = Localize.iconTrayExit();
}
private void InitializeContextMenu()
{
var menu = _contextMenu;
menu.Items.Clear();
var openIcon = new FontIcon { Glyph = "\ue71e" };
var open = new MenuItem
{
Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")",
Icon = openIcon
};
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };
var gamemode = new MenuItem
{
Header = Localize.GameMode(),
Icon = gamemodeIcon
};
var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
var positionreset = new MenuItem
{
Header = Localize.PositionReset(),
Icon = positionresetIcon
};
var settingsIcon = new FontIcon { Glyph = "\ue713" };
var settings = new MenuItem
{
Header = Localize.iconTraySettings(),
Icon = settingsIcon
};
var exitIcon = new FontIcon { Glyph = "\ue7e8" };
var exit = new MenuItem
{
Header = Localize.iconTrayExit(),
Icon = exitIcon
};
open.Click += (o, e) => _viewModel.ToggleFlowLauncher();
gamemode.Click += (o, e) => _viewModel.ToggleGameMode();
positionreset.Click += (o, e) => _ = PositionResetAsync();
settings.Click += (o, e) => App.API.OpenSettingDialog();
exit.Click += (o, e) => Close();
gamemode.ToolTip = Localize.GameModeToolTip();
positionreset.ToolTip = Localize.PositionResetToolTip();
_contextMenu.Items.Add(open);
_contextMenu.Items.Add(gamemode);
_contextMenu.Items.Add(positionreset);
_contextMenu.Items.Add(settings);
_contextMenu.Items.Add(exit);
}
#endregion
#region Window Position
public void UpdatePosition()
{
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
InitializeDialogJumpPosition();
InitializeDialogJumpPosition();
}
else
{
InitializePosition();
InitializePosition();
}
}
private async Task PositionResetAsync()
{
_viewModel.Show();
await Task.Delay(300); // If don't give a time, Positioning will be weird.
var screen = SelectedScreen();
Left = HorizonCenter(screen);
Top = VerticalCenter(screen);
}
private void InitializePosition()
{
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
InitializePositionInner();
InitializePositionInner();
return;
void InitializePositionInner()
{
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
{
var previousScreenWidth = _settings.PreviousScreenWidth;
var previousScreenHeight = _settings.PreviousScreenHeight;
GetDpi(out var previousDpiX, out var previousDpiY);
_settings.PreviousScreenWidth = SystemParameters.VirtualScreenWidth;
_settings.PreviousScreenHeight = SystemParameters.VirtualScreenHeight;
GetDpi(out var currentDpiX, out var currentDpiY);
if (previousScreenWidth != 0 && previousScreenHeight != 0 &&
previousDpiX != 0 && previousDpiY != 0 &&
(previousScreenWidth != SystemParameters.VirtualScreenWidth ||
previousScreenHeight != SystemParameters.VirtualScreenHeight ||
previousDpiX != currentDpiX || previousDpiY != currentDpiY))
{
AdjustPositionForResolutionChange();
return;
}
Left = _settings.WindowLeft;
Top = _settings.WindowTop;
}
else
{
var screen = SelectedScreen();
switch (_settings.SearchWindowAlign)
{
case SearchWindowAligns.Center:
Left = HorizonCenter(screen);
Top = VerticalCenter(screen);
break;
case SearchWindowAligns.CenterTop:
Left = HorizonCenter(screen);
Top = VerticalTop(screen);
break;
case SearchWindowAligns.LeftTop:
Left = HorizonLeft(screen);
Top = VerticalTop(screen);
break;
case SearchWindowAligns.RightTop:
Left = HorizonRight(screen);
Top = VerticalTop(screen);
break;
case SearchWindowAligns.Custom:
var customLeft = Win32Helper.TransformPixelsToDIP(this,
screen.WorkingArea.X + _settings.CustomWindowLeft, 0);
var customTop = Win32Helper.TransformPixelsToDIP(this, 0,
screen.WorkingArea.Y + _settings.CustomWindowTop);
Left = customLeft.X;
Top = customTop.Y;
break;
}
}
}
}
private void AdjustPositionForResolutionChange()
{
var screenWidth = SystemParameters.VirtualScreenWidth;
var screenHeight = SystemParameters.VirtualScreenHeight;
GetDpi(out var currentDpiX, out var currentDpiY);
var previousLeft = _settings.WindowLeft;
var previousTop = _settings.WindowTop;
GetDpi(out var previousDpiX, out var previousDpiY);
var widthRatio = screenWidth / _settings.PreviousScreenWidth;
var heightRatio = screenHeight / _settings.PreviousScreenHeight;
var dpiXRatio = currentDpiX / previousDpiX;
var dpiYRatio = currentDpiY / previousDpiY;
var newLeft = previousLeft * widthRatio * dpiXRatio;
var newTop = previousTop * heightRatio * dpiYRatio;
var screenLeft = SystemParameters.VirtualScreenLeft;
var screenTop = SystemParameters.VirtualScreenTop;
var maxX = screenLeft + screenWidth - ActualWidth;
var maxY = screenTop + screenHeight - ActualHeight;
Left = Math.Max(screenLeft, Math.Min(newLeft, maxX));
Top = Math.Max(screenTop, Math.Min(newTop, maxY));
}
private void GetDpi(out double dpiX, out double dpiY)
{
var source = PresentationSource.FromVisual(this);
if (source != null && source.CompositionTarget != null)
{
var matrix = source.CompositionTarget.TransformToDevice;
dpiX = 96 * matrix.M11;
dpiY = 96 * matrix.M22;
}
else
{
dpiX = 96;
dpiY = 96;
}
}
private MonitorInfo SelectedScreen()
{
MonitorInfo screen;
switch (_settings.SearchWindowScreen)
{
case SearchWindowScreens.Cursor:
screen = MonitorInfo.GetCursorDisplayMonitor();
break;
case SearchWindowScreens.Focus:
screen = MonitorInfo.GetNearestDisplayMonitor(Win32Helper.GetForegroundWindow());
break;
case SearchWindowScreens.Primary:
screen = MonitorInfo.GetPrimaryDisplayMonitor();
break;
case SearchWindowScreens.Custom:
var allScreens = MonitorInfo.GetDisplayMonitors();
if (_settings.CustomScreenNumber <= allScreens.Count)
screen = allScreens[_settings.CustomScreenNumber - 1];
else
screen = allScreens[0];
break;
default:
screen = MonitorInfo.GetDisplayMonitors()[0];
break;
}
return screen ?? MonitorInfo.GetDisplayMonitors()[0];
}
private double HorizonCenter(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}
private double VerticalCenter(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y;
return top;
}
private double HorizonRight(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var left = (dip1.X + dip2.X - ActualWidth) - 10;
return left;
}
private double HorizonLeft(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var left = dip1.X + 10;
return left;
}
private double VerticalTop(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var top = dip1.Y + 10;
return top;
}
#endregion
#region Window Animation
private void InitProgressbarAnimation()
{
var progressBarStoryBoard = new Storyboard();
var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100,
new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth + 0,
new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)"));
Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)"));
progressBarStoryBoard.Children.Add(da);
progressBarStoryBoard.Children.Add(da1);
progressBarStoryBoard.RepeatBehavior = RepeatBehavior.Forever;
da.Freeze();
da1.Freeze();
const string progressBarAnimationName = "ProgressBarAnimation";
var beginStoryboard = new BeginStoryboard
{
Name = progressBarAnimationName, Storyboard = progressBarStoryBoard
};
var stopStoryboard = new StopStoryboard()
{
BeginStoryboardName = progressBarAnimationName
};
var trigger = new Trigger
{
Property = VisibilityProperty, Value = Visibility.Visible
};
trigger.EnterActions.Add(beginStoryboard);
trigger.ExitActions.Add(stopStoryboard);
var progressStyle = new Style(typeof(Line))
{
BasedOn = FindResource("PendingLineStyle") as Style
};
progressStyle.RegisterName(progressBarAnimationName, beginStoryboard);
progressStyle.Triggers.Add(trigger);
ProgressBar.Style = progressStyle;
_viewModel.ProgressBarVisibility = Visibility.Hidden;
}
private void WindowAnimation()
{
_isArrowKeyPressed = true;
var clocksb = new Storyboard();
var iconsb = new Storyboard();
var easing = new CircleEase { EasingMode = EasingMode.EaseInOut };
var animationLength = _settings.AnimationSpeed switch
{
AnimationSpeeds.Slow => 560,
AnimationSpeeds.Medium => 360,
AnimationSpeeds.Fast => 160,
_ => _settings.CustomAnimationLength
};
var IconMotion = new DoubleAnimation
{
From = 12,
To = 0,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
};
var ClockOpacity = new DoubleAnimation
{
From = 0,
To = 1,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
};
var TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0);
var IconOpacity = new DoubleAnimation
{
From = 0,
To = TargetIconOpacity,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
};
var rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right;
var thicknessAnimation = new ThicknessAnimation
{
From = new Thickness(0, 12, rightMargin, 0),
To = new Thickness(0, 0, rightMargin, 0),
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
};
Storyboard.SetTarget(ClockOpacity, ClockPanel);
Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty));
Storyboard.SetTarget(thicknessAnimation, ClockPanel);
Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty));
Storyboard.SetTarget(IconMotion, SearchIcon);
Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty));
Storyboard.SetTarget(IconOpacity, SearchIcon);
Storyboard.SetTargetProperty(IconOpacity, new PropertyPath(OpacityProperty));
clocksb.Children.Add(thicknessAnimation);
clocksb.Children.Add(ClockOpacity);
iconsb.Children.Add(IconMotion);
iconsb.Children.Add(IconOpacity);
_settings.WindowLeft = Left;
_isArrowKeyPressed = false;
clocksb.Begin(ClockPanel);
iconsb.Begin(SearchIcon);
}
private void UpdateClockPanelVisibility()
{
if (QueryTextBox == null || ResultContextMenu == null || History == null || ClockPanel == null)
{
return;
}
// ✅ Initialize animation length & duration
var animationLength = _settings.AnimationSpeed switch
{
AnimationSpeeds.Slow => 560,
AnimationSpeeds.Medium => 360,
AnimationSpeeds.Fast => 160,
_ => _settings.CustomAnimationLength
};
var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3);
// ✅ Conditions for showing ClockPanel (No query input / ResultContextMenu & History are closed)
var shouldShowClock = QueryTextBox.Text.Length == 0 &&
ResultContextMenu.Visibility != Visibility.Visible &&
History.Visibility != Visibility.Visible;
// ✅ 1. When ResultContextMenu opens, immediately set Visibility.Hidden (force hide without animation)
if (ResultContextMenu.Visibility == Visibility.Visible)
{
_viewModel.ClockPanelVisibility = Visibility.Hidden;
_viewModel.ClockPanelOpacity = 0.0; // Set to 0 in case Opacity animation affects it
return;
}
// ✅ 2. When ResultContextMenu is closed, keep it Hidden if there's text in the query (remember previous state)
else if (QueryTextBox.Text.Length > 0)
{
_viewModel.ClockPanelVisibility = Visibility.Hidden;
_viewModel.ClockPanelOpacity = 0.0;
return;
}
// ✅ Prevent multiple animations
if (_isClockPanelAnimating)
{
return;
}
// ✅ 3. When hiding ClockPanel (apply fade-out animation)
if ((!shouldShowClock) && _viewModel.ClockPanelVisibility == Visibility.Visible)
{
_isClockPanelAnimating = true;
var fadeOut = new DoubleAnimation
{
From = 1.0,
To = 0.0,
Duration = animationDuration,
FillBehavior = FillBehavior.HoldEnd
};
fadeOut.Completed += (s, e) =>
{
_viewModel.ClockPanelVisibility = Visibility.Hidden; // ✅ Completely hide after animation
_isClockPanelAnimating = false;
};
ClockPanel.BeginAnimation(OpacityProperty, fadeOut);
}
// ✅ 4. When showing ClockPanel (apply fade-in animation)
else if (shouldShowClock && _viewModel.ClockPanelVisibility != Visibility.Visible)
{
_isClockPanelAnimating = true;
_viewModel.ClockPanelVisibility = Visibility.Visible; // ✅ Set Visibility to Visible first
var fadeIn = new DoubleAnimation
{
From = 0.0,
To = 1.0,
Duration = animationDuration,
FillBehavior = FillBehavior.HoldEnd
};
fadeIn.Completed += (s, e) => _isClockPanelAnimating = false;
ClockPanel.BeginAnimation(OpacityProperty, fadeIn);
}
}
private static double GetOpacityFromStyle(Style style, double defaultOpacity = 1.0)
{
if (style == null)
{
return defaultOpacity;
}
foreach (Setter setter in style.Setters.Cast<Setter>())
{
if (setter.Property == OpacityProperty)
{
return setter.Value is double opacity ? opacity : defaultOpacity;
}
}
return defaultOpacity;
}
private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness)
{
if (style == null)
{
return defaultThickness;
}
foreach (Setter setter in style.Setters.Cast<Setter>())
{
if (setter.Property == MarginProperty)
{
return setter.Value is Thickness thickness ? thickness : defaultThickness;
}
}
return defaultThickness;
}
#endregion
#region QueryTextBox Event
private void QueryTextBox_OnCopy(object sender, ExecutedRoutedEventArgs e)
{
var result = _viewModel.Results.SelectedItem?.Result;
if (QueryTextBox.SelectionLength == 0 && result != null)
{
string copyText = result.CopyText;
App.API.CopyToClipboard(copyText, directCopy: true);
}
else if (!string.IsNullOrEmpty(QueryTextBox.Text))
{
App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false);
}
}
private void QueryTextBox_OnPaste(object sender, DataObjectPastingEventArgs e)
{
try
{
var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true);
if (isText)
{
var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string;
text = text.Replace(Environment.NewLine, " ");
DataObject data = new DataObject();
data.SetData(DataFormats.UnicodeText, text);
e.DataObject = data;
}
}
catch (Exception ex)
{
App.API.LogException(ClassName, "Failed to paste text", ex);
}
}
private void QueryTextBox_KeyUp(object sender, KeyEventArgs e)
{
if (_viewModel.QueryText != QueryTextBox.Text)
{
BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
}
}
private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e)
{
e.Handled = true;
}
#endregion
#region Placeholder
private void SetupPlaceholderText()
{
if (_settings.ShowPlaceholder)
{
QueryTextBox.TextChanged += QueryTextBox_TextChanged;
QueryTextSuggestionBox.TextChanged += QueryTextSuggestionBox_TextChanged;
SetPlaceholderText();
}
else
{
QueryTextBox.TextChanged -= QueryTextBox_TextChanged;
QueryTextSuggestionBox.TextChanged -= QueryTextSuggestionBox_TextChanged;
QueryTextPlaceholderBox.Visibility = Visibility.Collapsed;
}
}
private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
SetPlaceholderText();
}
private void QueryTextSuggestionBox_TextChanged(object sender, TextChangedEventArgs e)
{
SetPlaceholderText();
}
private void SetPlaceholderText()
{
var queryText = QueryTextBox.Text;
var suggestionText = QueryTextSuggestionBox.Text;
QueryTextPlaceholderBox.Visibility = string.IsNullOrEmpty(queryText) && string.IsNullOrEmpty(suggestionText) ? Visibility.Visible : Visibility.Collapsed;
}
#endregion
#region Resize Mode
private void SetupResizeMode()
{
ResizeMode = _settings.KeepMaxResults ? ResizeMode.NoResize : ResizeMode.CanResize;
if (WindowChrome.GetWindowChrome(this) is WindowChrome windowChrome)
{
_theme.SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults);
}
}
#endregion
#region Search Delay
private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e)
{
var textBox = (TextBox)sender;
_viewModel.QueryText = textBox.Text;
_viewModel.Query(_settings.SearchQueryResultsWithDelay);
}
#endregion
#region Dialog Jump
private void InitializeDialogJump()
{
DialogJump.ShowDialogJumpWindowAsync = _viewModel.SetupDialogJumpAsync;
DialogJump.UpdateDialogJumpWindow = InitializeDialogJumpPosition;
DialogJump.ResetDialogJumpWindow = _viewModel.ResetDialogJump;
DialogJump.HideDialogJumpWindow = _viewModel.HideDialogJump;
}
private void InitializeDialogJumpPosition()
{
if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return;
if (!_viewModel.IsDialogJumpWindowUnderDialog()) return;
// Get dialog window rect
var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window);
if (!result) return;
// Move window below the bottom of the dialog and keep it center
Top = VerticalBottom(window);
Left = HorizonCenter(window);
}
private double HorizonCenter(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}
private double VerticalBottom(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom);
return dip1.Y;
}
#endregion
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_hwndSource?.Dispose();
_notifyIcon?.Dispose();
_animationSoundWMP?.Close();
_animationSoundWPF?.Dispose();
_viewModel.ActualApplicationThemeChanged -= ViewModel_ActualApplicationThemeChanged;
UnregisterSoundEffectsEvent();
}
_disposed = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}