Flow.Launcher/Flow.Launcher/MainWindow.xaml.cs

1503 lines
56 KiB
C#
Raw Permalink Normal View History

using System;
using System.ComponentModel;
2025-03-17 01:41:22 +00:00
using System.Linq;
using System.Media;
2025-09-30 11:41:27 +00:00
using System.Threading;
using System.Threading.Tasks;
2014-01-29 10:33:24 +00:00
using System.Windows;
2025-03-17 01:41:22 +00:00
using System.Windows.Controls;
using System.Windows.Data;
2014-01-29 10:33:24 +00:00
using System.Windows.Input;
2025-03-17 01:41:22 +00:00
using System.Windows.Interop;
using System.Windows.Media;
2014-01-29 10:33:24 +00:00
using System.Windows.Media.Animation;
2025-03-17 01:41:22 +00:00
using System.Windows.Shapes;
using System.Windows.Shell;
2025-03-17 01:41:22 +00:00
using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
2020-04-21 09:12:17 +00:00
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
2021-12-03 17:23:00 +00:00
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.DialogJump;
2025-03-17 01:41:22 +00:00
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
2022-01-19 20:43:00 +00:00
using Flow.Launcher.Plugin.SharedCommands;
2025-07-23 05:51:35 +00:00
using Flow.Launcher.Plugin.SharedModels;
2025-03-17 01:41:22 +00:00
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern;
using iNKORE.UI.WPF.Modern.Controls;
2024-02-04 21:02:53 +00:00
using DataObject = System.Windows.DataObject;
2025-03-20 23:57:27 +00:00
using Key = System.Windows.Input.Key;
2025-03-17 01:41:22 +00:00
using MouseButtons = System.Windows.Forms.MouseButtons;
using NotifyIcon = System.Windows.Forms.NotifyIcon;
2014-01-29 10:33:24 +00:00
2020-04-21 09:12:17 +00:00
namespace Flow.Launcher
2014-07-23 20:46:19 +00:00
{
public partial class MainWindow : IDisposable
2014-07-23 20:46:19 +00:00
{
2025-04-03 15:01:16 +00:00
#region Public Property
// Window Event: Close Event
public bool CanClose { get; set; } = false;
#endregion
2016-02-26 23:43:57 +00:00
#region Private Fields
2014-07-23 20:46:19 +00:00
2025-07-11 14:24:05 +00:00
// Class Name
private static readonly string ClassName = nameof(MainWindow);
2025-03-17 12:09:02 +00:00
// Dependency Injection
2025-03-16 07:05:15 +00:00
private readonly Settings _settings;
2025-03-16 15:55:15 +00:00
private readonly Theme _theme;
2025-03-17 12:09:02 +00:00
// Window Notify Icon
private NotifyIcon _notifyIcon;
2025-03-17 12:09:02 +00:00
// Window Context Menu
2025-03-22 05:26:12 +00:00
private readonly ContextMenu _contextMenu = new();
2025-03-16 07:05:15 +00:00
private readonly MainViewModel _viewModel;
2025-03-17 12:09:02 +00:00
2025-03-22 02:24:34 +00:00
// Window Event: Key Event
2025-03-22 05:26:12 +00:00
private bool _isArrowKeyPressed = false;
2025-03-17 12:09:02 +00:00
// Window Sound Effects
2025-09-30 11:41:27 +00:00
private MediaPlayer _animationSoundWMP;
private SoundPlayer _animationSoundWPF;
private readonly Lock _soundLock = new();
2025-03-17 12:09:02 +00:00
// Window WndProc
private HwndSource _hwndSource;
2025-03-16 15:10:12 +00:00
private int _initialWidth;
private int _initialHeight;
2025-03-17 12:09:02 +00:00
// Window Animation
private const double DefaultRightMargin = 66; //* this value from base.xaml
2025-03-21 05:34:52 +00:00
private bool _isClockPanelAnimating = false;
// IDisposable
2025-03-22 05:26:12 +00:00
private bool _disposed = false;
2014-07-23 20:46:19 +00:00
#endregion
2016-02-26 23:43:57 +00:00
2025-03-16 15:10:12 +00:00
#region Constructor
2025-03-16 15:55:15 +00:00
public MainWindow()
{
2025-03-16 15:55:15 +00:00
_settings = Ioc.Default.GetRequiredService<Settings>();
_theme = Ioc.Default.GetRequiredService<Theme>();
_viewModel = Ioc.Default.GetRequiredService<MainViewModel>();
DataContext = _viewModel;
2025-05-13 13:54:03 +00:00
Topmost = _settings.ShowAtTopmost;
InitializeComponent();
UpdatePosition();
2024-05-19 12:13:37 +00:00
InitSoundEffects();
RegisterSoundEffectsEvent();
2025-03-16 15:10:12 +00:00
DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste);
_viewModel.ActualApplicationThemeChanged += ViewModel_ActualApplicationThemeChanged;
}
2025-03-16 15:10:12 +00:00
#endregion
2025-03-16 15:10:12 +00:00
#region Window Event
2025-03-17 01:35:42 +00:00
#pragma warning disable VSTHRD100 // Avoid async void methods
private void ViewModel_ActualApplicationThemeChanged(object sender, ActualApplicationThemeChangedEventArgs args)
2025-05-14 04:09:18 +00:00
{
_ = _theme.RefreshFrameAsync();
2025-05-14 04:09:18 +00:00
}
2025-03-16 15:10:12 +00:00
private void OnSourceInitialized(object sender, EventArgs e)
{
var handle = Win32Helper.GetWindowHandle(this, true);
_hwndSource = HwndSource.FromHwnd(handle);
_hwndSource.AddHook(WndProc);
2025-03-16 15:10:12 +00:00
Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this);
2024-05-14 07:54:15 +00:00
}
2024-05-25 15:55:41 +00:00
private void OnLoaded(object sender, RoutedEventArgs e)
2024-05-14 07:54:15 +00:00
{
2025-03-16 15:10:12 +00:00
// Check first launch
if (_settings.FirstLaunch)
2024-05-14 07:54:15 +00:00
{
2025-03-28 04:33:07 +00:00
// Set First Launch to false
2025-03-16 15:10:12 +00:00
_settings.FirstLaunch = false;
2025-03-28 04:33:07 +00:00
2025-06-07 03:24:35 +00:00
// Update release notes version
_settings.ReleaseNotesVersion = Constant.Version;
2025-03-28 04:33:07 +00:00
// Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None
if (Win32Helper.IsBackdropSupported()) _settings.BackdropType = BackdropTypes.Acrylic;
// Save settings
2025-03-16 15:10:12 +00:00
App.API.SaveAppAllSettings();
2025-03-28 04:33:07 +00:00
// Show Welcome Window
2025-03-28 13:46:23 +00:00
var welcomeWindow = new WelcomeWindow();
welcomeWindow.Show();
2024-05-14 07:54:15 +00:00
}
if (Constant.Version != "1.0.0" && _settings.ReleaseNotesVersion != Constant.Version) // Skip release notes notification for developer builds (version 1.0.0)
2025-06-07 03:24:35 +00:00
{
// Update release notes version
_settings.ReleaseNotesVersion = Constant.Version;
// Show release note popup with button
2025-06-09 06:42:48 +00:00
App.API.ShowMsgWithButton(
Localize.appUpdateTitle(Constant.Version),
Localize.appUpdateButtonContent(),
2025-06-09 06:42:48 +00:00
() =>
{
Application.Current.Dispatcher.Invoke(() =>
{
var releaseNotesWindow = new ReleaseNotesWindow();
releaseNotesWindow.Show();
});
});
2025-06-07 03:24:35 +00:00
}
2025-06-06 15:08:41 +00:00
2025-03-29 05:10:06 +00:00
// Initialize place holder
SetupPlaceholderText();
2025-03-29 07:08:49 +00:00
_viewModel.PlaceholderText = _settings.PlaceholderText;
2025-03-29 05:10:06 +00:00
2025-03-16 15:10:12 +00:00
// Hide window if need
UpdatePosition();
2025-03-16 15:10:12 +00:00
if (_settings.HideOnStartup)
2024-05-27 07:09:22 +00:00
{
2025-03-16 15:10:12 +00:00
_viewModel.Hide();
_viewModel.InitializeVisibilityStatus(false);
2024-05-14 07:54:15 +00:00
}
2025-03-16 15:10:12 +00:00
else
2021-12-02 08:55:47 +00:00
{
2025-03-16 15:10:12 +00:00
_viewModel.Show();
_viewModel.InitializeVisibilityStatus(true);
2025-04-25 08:32:23 +00:00
// 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();
2025-03-16 15:10:12 +00:00
InitializeNotifyIcon();
2024-05-25 15:55:41 +00:00
2025-03-16 15:10:12 +00:00
// Initialize color scheme
if (_settings.ColorScheme == Constant.Light)
{
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Light;
}
2025-03-16 15:10:12 +00:00
else if (_settings.ColorScheme == Constant.Dark)
2023-12-26 14:46:46 +00:00
{
ThemeManager.Current.ApplicationTheme = ApplicationTheme.Dark;
2023-12-26 14:46:46 +00:00
}
2024-05-27 07:09:22 +00:00
2025-03-16 15:10:12 +00:00
// Force update position
UpdatePosition();
// Initialize resize mode after refreshing frame
SetupResizeMode();
2025-03-16 15:10:12 +00:00
// Reset preview
_viewModel.ResetPreview();
2025-03-30 04:20:14 +00:00
2025-03-16 09:11:04 +00:00
// 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();
2025-03-21 05:34:52 +00:00
// View model property changed event
_viewModel.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
{
case nameof(MainViewModel.MainWindowVisibilityStatus):
{
2025-03-15 13:58:55 +00:00
Dispatcher.Invoke(() =>
{
2025-03-15 13:58:55 +00:00
if (_viewModel.MainWindowVisibilityStatus)
{
2025-03-28 04:00:23 +00:00
// Play sound effect before activing the window
if (_settings.UseSound && !_viewModel.IsDialogJumpWindowUnderDialog())
2025-03-15 13:58:55 +00:00
{
SoundPlay();
}
2025-03-16 15:10:12 +00:00
2025-03-28 04:00:23 +00:00
// Update position & Activate
UpdatePosition();
2025-03-15 13:58:55 +00:00
Activate();
2025-03-28 04:00:23 +00:00
// Reset preview
_viewModel.ResetPreview();
// Select last query if need
2025-03-15 13:58:55 +00:00
if (!_viewModel.LastQuerySelected)
{
QueryTextBox.SelectAll();
_viewModel.LastQuerySelected = true;
}
2025-03-28 04:00:23 +00:00
// Focus query box
QueryTextBox.Focus();
// Play window animation
if (_settings.UseAnimation && !_viewModel.IsDialogJumpWindowUnderDialog())
2025-03-28 04:00:23 +00:00
{
2025-03-16 15:10:12 +00:00
WindowAnimation();
2025-03-28 04:00:23 +00:00
}
2025-03-28 04:33:07 +00:00
// Update activate times
2025-03-28 04:00:23 +00:00
_settings.ActivateTimes++;
}
2025-03-17 23:43:34 +00:00
});
break;
}
case nameof(MainViewModel.QueryTextCursorMovedToEnd):
if (_viewModel.QueryTextCursorMovedToEnd)
{
2025-03-16 15:10:12 +00:00
// 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;
2022-12-23 06:14:53 +00:00
case nameof(MainViewModel.GameModeStatus):
_notifyIcon.Icon = _viewModel.GameModeStatus
? Properties.Resources.gamemode
: Properties.Resources.app;
2022-12-23 06:14:53 +00:00
break;
}
};
2022-12-23 06:14:53 +00:00
2025-03-21 05:34:52 +00:00
// Settings property changed event
2018-12-19 03:46:27 +00:00
_settings.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
2018-12-19 03:46:27 +00:00
{
case nameof(Settings.HideNotifyIcon):
_notifyIcon.Visible = !_settings.HideNotifyIcon;
break;
case nameof(Settings.Language):
UpdateNotifyIconText();
2025-07-09 03:12:46 +00:00
if (_settings.ShowHomePage && _viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
}
break;
2021-12-19 13:55:17 +00:00
case nameof(Settings.Hotkey):
UpdateNotifyIconText();
break;
case nameof(Settings.WindowLeft):
Left = _settings.WindowLeft;
break;
case nameof(Settings.WindowTop):
Top = _settings.WindowTop;
2022-10-19 10:25:00 +00:00
break;
2025-03-29 05:10:06 +00:00
case nameof(Settings.ShowPlaceholder):
SetupPlaceholderText();
break;
2025-03-29 07:08:49 +00:00
case nameof(Settings.PlaceholderText):
_viewModel.PlaceholderText = _settings.PlaceholderText;
break;
case nameof(Settings.KeepMaxResults):
2025-03-29 05:25:00 +00:00
SetupResizeMode();
2025-03-30 04:07:04 +00:00
break;
case nameof(Settings.SettingWindowFont):
InitializeContextMenu();
break;
case nameof(Settings.ShowHomePage):
case nameof(Settings.ShowHistoryResultsForHomePage):
case nameof(Settings.HistoryStyle):
2025-05-04 01:07:14 +00:00
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
{
_viewModel.QueryResults();
}
break;
2025-05-13 13:54:03 +00:00
case nameof(Settings.ShowAtTopmost):
Topmost = _settings.ShowAtTopmost;
break;
2018-12-19 03:46:27 +00:00
}
};
2025-03-19 10:03:02 +00:00
2025-03-22 05:40:33 +00:00
// QueryTextBox.Text change detection (modified to only work when character count is 1 or higher)
2025-03-28 13:42:40 +00:00
QueryTextBox.TextChanged += (s, e) => UpdateClockPanelVisibility();
2025-03-06 11:49:14 +00:00
// Detecting ResultContextMenu.Visibility changes
2025-03-06 11:49:14 +00:00
DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(ResultListBox))
.AddValueChanged(ResultContextMenu, (s, e) => UpdateClockPanelVisibility());
2025-03-06 11:49:14 +00:00
2025-03-22 05:40:33 +00:00
// Detect History.Visibility changes
2025-03-06 11:49:14 +00:00
DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(ResultListBox))
2025-03-06 11:49:14 +00:00
.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();
}
2025-03-16 15:10:12 +00:00
private async void OnClosing(object sender, CancelEventArgs e)
{
2025-04-03 15:01:16 +00:00
if (!CanClose)
2025-03-22 05:26:12 +00:00
{
2025-04-11 14:05:04 +00:00
CanClose = true;
2025-03-22 05:26:12 +00:00
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
2025-04-11 14:05:04 +00:00
// 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
2025-03-28 06:31:46 +00:00
Application.Current.Shutdown();
2025-03-22 05:26:12 +00:00
}
}
private void OnClosed(object sender, EventArgs e)
{
try
{
_hwndSource.RemoveHook(WndProc);
}
catch (Exception)
{
// Ignored
}
_hwndSource = null;
2024-06-05 22:37:13 +00:00
}
2025-03-16 15:10:12 +00:00
private void OnLocationChanged(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
if (IsLoaded)
{
2025-03-16 15:10:12 +00:00
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
}
}
2025-03-16 15:10:12 +00:00
private async void OnDeactivated(object sender, EventArgs e)
{
if (_viewModel.IsDialogJumpWindowUnderDialog())
{
return;
}
2025-03-16 15:10:12 +00:00
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
_viewModel.ClockPanelOpacity = 0.0;
_viewModel.SearchIconOpacity = 0.0;
// This condition stops extra hide call when animator is on,
2025-03-16 15:10:12 +00:00
// which causes the toggling to occasional hide instead of show.
if (_viewModel.MainWindowVisibilityStatus)
2021-10-27 02:49:00 +00:00
{
2025-03-16 15:10:12 +00:00
// 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)
2025-03-28 04:00:23 +00:00
{
2025-03-16 15:10:12 +00:00
await Task.Delay(100);
2025-03-28 04:00:23 +00:00
}
2022-11-21 17:49:10 +00:00
2025-03-16 15:10:12 +00:00
if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible)
2024-06-04 22:03:26 +00:00
{
2025-03-16 15:10:12 +00:00
_viewModel.Hide();
}
2025-03-16 15:10:12 +00:00
}
}
2025-03-16 15:10:12 +00:00
private void OnKeyDown(object sender, KeyEventArgs e)
{
var specialKeyState = GlobalHotkey.CheckModifiers();
switch (e.Key)
{
case Key.Down:
2025-03-22 05:26:12 +00:00
_isArrowKeyPressed = true;
2025-03-16 15:10:12 +00:00
_viewModel.SelectNextItemCommand.Execute(null);
e.Handled = true;
break;
case Key.Up:
2025-03-22 05:26:12 +00:00
_isArrowKeyPressed = true;
2025-03-16 15:10:12 +00:00
_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:
2025-03-26 02:07:35 +00:00
if (_viewModel.QueryResultsSelected()
2025-03-16 15:10:12 +00:00
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length
&& !string.IsNullOrEmpty(QueryTextBox.Text))
{
_viewModel.LoadContextMenuCommand.Execute(null);
e.Handled = true;
}
break;
case Key.Left:
2025-03-26 02:07:35 +00:00
if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
2025-03-16 15:10:12 +00:00
{
_viewModel.EscCommand.Execute(null);
e.Handled = true;
}
break;
case Key.Back:
if (specialKeyState.CtrlPressed)
{
2025-03-26 02:07:35 +00:00
if (_viewModel.QueryResultsSelected()
2025-03-16 15:10:12 +00:00
&& QueryTextBox.Text.Length > 0
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text, QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
2025-03-16 15:10:12 +00:00
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)
{
2025-03-22 05:26:12 +00:00
_isArrowKeyPressed = false;
2025-03-16 15:10:12 +00:00
}
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
2025-03-22 05:26:12 +00:00
if (_isArrowKeyPressed)
2025-03-16 15:10:12 +00:00
{
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
}
}
2025-03-17 01:35:42 +00:00
#pragma warning restore VSTHRD100 // Avoid async void methods
2025-03-16 15:10:12 +00:00
#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)
{
2025-06-02 10:16:01 +00:00
// 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;
2025-06-02 10:16:01 +00:00
// Current monitor information
var screen = MonitorInfo.GetNearestDisplayMonitor(new WindowInteropHelper(this).Handle);
var workingArea = screen.WorkingArea;
var screenLeftTop = Win32Helper.TransformPixelsToDIP(this, workingArea.X, workingArea.Y);
2025-06-02 10:16:01 +00:00
// Switch to Normal state
WindowState = WindowState.Normal;
Application.Current?.Dispatcher.Invoke(new Action(() =>
{
double normalWidth = Width;
double normalHeight = Height;
2025-06-02 10:16:01 +00:00
// 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)
{
2025-06-02 10:16:01 +00:00
// Ignored - can occur if drag operation is already in progress
}
}
2025-03-16 15:10:12 +00:00
}
#endregion
#region Window Context Menu Event
2025-03-17 01:35:42 +00:00
#pragma warning disable VSTHRD100 // Avoid async void methods
2025-03-16 15:10:12 +00:00
private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e)
{
_viewModel.Hide();
if (_settings.UseAnimation)
await Task.Delay(100);
App.API.OpenSettingDialog();
}
2025-03-17 01:35:42 +00:00
#pragma warning restore VSTHRD100 // Avoid async void methods
2025-03-16 15:10:12 +00:00
#endregion
#region Window WndProc
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
2025-03-16 15:10:12 +00:00
{
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))
2025-03-16 15:10:12 +00:00
{
if (!_settings.KeepMaxResults)
2025-03-29 07:38:44 +00:00
{
// Get shadow margin
var shadowMargin = 0;
var (_, useDropShadowEffect) = _theme.GetActualValue();
if (useDropShadowEffect)
{
shadowMargin = 32;
}
2025-03-16 15:10:12 +00:00
// 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));
}
2025-03-16 15:10:12 +00:00
}
SizeToContent = SizeToContent.Height;
}
else
2025-03-29 07:38:44 +00:00
{
// Update height when exiting maximized snap state.
SizeToContent = SizeToContent.Height;
2025-03-29 07:38:44 +00:00
}
if (_initialWidth != (int)Width)
{
if (!_settings.KeepMaxResults)
{
// Update width
_viewModel.MainWindowWidth = Width;
}
2025-03-16 15:10:12 +00:00
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;
}
2025-03-16 15:10:12 +00:00
return IntPtr.Zero;
2024-06-05 22:37:13 +00:00
}
2024-06-19 18:38:34 +00:00
2025-03-16 15:10:12 +00:00
#endregion
#region Window Sound Effects
private void InitSoundEffects()
{
2025-09-30 11:41:27 +00:00
lock (_soundLock)
2025-03-16 15:10:12 +00:00
{
2025-09-30 11:41:27 +00:00
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();
}
2025-03-16 15:10:12 +00:00
}
}
2025-03-16 15:10:12 +00:00
private void SoundPlay()
{
2025-09-30 11:41:27 +00:00
lock (_soundLock)
2025-03-16 15:10:12 +00:00
{
2025-09-30 11:41:27 +00:00
if (_settings.WMPInstalled)
{
_animationSoundWMP.Position = TimeSpan.Zero;
_animationSoundWMP.Volume = _settings.SoundVolume / 100.0;
_animationSoundWMP.Play();
}
else
{
_animationSoundWPF.Play();
}
2024-06-04 22:03:26 +00:00
}
}
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
2025-03-16 15:10:12 +00:00
{
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();
});
2025-03-16 15:10:12 +00:00
}
catch (Exception e)
{
App.API.LogException(ClassName, "Failed to register sound effect event", e);
}
}
private static void UnregisterSoundEffectsEvent()
{
try
{
Win32Helper.UnregisterSleepModeListener();
}
catch (Exception e)
2025-03-16 15:10:12 +00:00
{
App.API.LogException(ClassName, "Failed to unregister sound effect event", e);
2025-03-16 15:10:12 +00:00
}
}
2025-03-16 15:10:12 +00:00
#endregion
#region Window Notify Icon
private void InitializeNotifyIcon()
{
_notifyIcon = new NotifyIcon
{
2025-03-16 07:05:15 +00:00
Text = Constant.FlowLauncherFullName,
2024-05-30 16:48:29 +00:00
Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app,
2018-12-19 03:46:27 +00:00
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;
2025-09-23 10:07:15 +00:00
((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" };
2021-10-27 02:49:00 +00:00
var open = new MenuItem
{
2025-09-23 10:07:15 +00:00
Header = Localize.iconTrayOpen() + " (" + _settings.Hotkey + ")",
Icon = openIcon
};
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };
2021-11-19 06:43:47 +00:00
var gamemode = new MenuItem
{
Header = Localize.GameMode(),
2025-03-14 11:32:48 +00:00
Icon = gamemodeIcon
2021-11-19 06:43:47 +00:00
};
var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
2022-09-07 02:31:00 +00:00
var positionreset = new MenuItem
{
Header = Localize.PositionReset(),
Icon = positionresetIcon
};
var settingsIcon = new FontIcon { Glyph = "\ue713" };
2021-10-27 02:49:00 +00:00
var settings = new MenuItem
{
Header = Localize.iconTraySettings(),
Icon = settingsIcon
};
var exitIcon = new FontIcon { Glyph = "\ue7e8" };
2021-10-27 02:49:00 +00:00
var exit = new MenuItem
{
Header = Localize.iconTrayExit(),
2025-03-14 11:32:48 +00:00
Icon = exitIcon
2021-10-27 02:49:00 +00:00
};
open.Click += (o, e) => _viewModel.ToggleFlowLauncher();
2022-12-23 06:14:53 +00:00
gamemode.Click += (o, e) => _viewModel.ToggleGameMode();
2025-03-16 14:24:06 +00:00
positionreset.Click += (o, e) => _ = PositionResetAsync();
settings.Click += (o, e) => App.API.OpenSettingDialog();
exit.Click += (o, e) => Close();
2022-11-21 17:49:10 +00:00
gamemode.ToolTip = Localize.GameModeToolTip();
positionreset.ToolTip = Localize.PositionResetToolTip();
2022-11-21 17:49:10 +00:00
2025-03-22 05:26:12 +00:00
_contextMenu.Items.Add(open);
_contextMenu.Items.Add(gamemode);
_contextMenu.Items.Add(positionreset);
_contextMenu.Items.Add(settings);
_contextMenu.Items.Add(exit);
2021-12-02 09:39:44 +00:00
}
2022-12-23 03:46:12 +00:00
2025-03-16 15:10:12 +00:00
#endregion
#region Window Position
public void UpdatePosition()
{
2025-10-15 04:24:36 +00:00
// 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();
}
}
2022-12-23 03:46:12 +00:00
2025-03-16 14:24:06 +00:00
private async Task PositionResetAsync()
2022-09-07 02:31:00 +00:00
{
_viewModel.Show();
await Task.Delay(300); // If don't give a time, Positioning will be weird.
2023-03-16 03:13:42 +00:00
var screen = SelectedScreen();
Left = HorizonCenter(screen);
Top = VerticalCenter(screen);
2022-09-07 02:31:00 +00:00
}
2022-12-23 03:46:12 +00:00
2025-03-16 15:10:12 +00:00
private void InitializePosition()
{
2025-10-15 04:24:36 +00:00
// Initialize call twice to workaround multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
2025-03-16 15:10:12 +00:00
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;
}
2025-03-16 15:10:12 +00:00
Left = _settings.WindowLeft;
Top = _settings.WindowTop;
2025-03-16 15:10:12 +00:00
}
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);
2025-03-16 15:10:12 +00:00
break;
case SearchWindowAligns.LeftTop:
Left = HorizonLeft(screen);
Top = VerticalTop(screen);
2025-03-16 15:10:12 +00:00
break;
case SearchWindowAligns.RightTop:
Left = HorizonRight(screen);
Top = VerticalTop(screen);
2025-03-16 15:10:12 +00:00
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;
2025-03-16 15:10:12 +00:00
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()
2025-03-16 15:10:12 +00:00
{
MonitorInfo screen;
2025-03-16 15:10:12 +00:00
switch (_settings.SearchWindowScreen)
{
case SearchWindowScreens.Cursor:
screen = MonitorInfo.GetCursorDisplayMonitor();
2025-03-16 15:10:12 +00:00
break;
case SearchWindowScreens.Focus:
screen = MonitorInfo.GetNearestDisplayMonitor(Win32Helper.GetForegroundWindow());
break;
case SearchWindowScreens.Primary:
screen = MonitorInfo.GetPrimaryDisplayMonitor();
2025-03-16 15:10:12 +00:00
break;
case SearchWindowScreens.Custom:
var allScreens = MonitorInfo.GetDisplayMonitors();
if (_settings.CustomScreenNumber <= allScreens.Count)
screen = allScreens[_settings.CustomScreenNumber - 1];
2025-03-16 15:10:12 +00:00
else
screen = allScreens[0];
2025-03-16 15:10:12 +00:00
break;
default:
screen = MonitorInfo.GetDisplayMonitors()[0];
2025-03-16 15:10:12 +00:00
break;
}
return screen ?? MonitorInfo.GetDisplayMonitors()[0];
2025-03-16 15:10:12 +00:00
}
private double HorizonCenter(MonitorInfo screen)
2025-03-16 15:10:12 +00:00
{
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)
2025-03-16 15:10:12 +00:00
{
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)
2025-03-16 15:10:12 +00:00
{
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)
2025-03-16 15:10:12 +00:00
{
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;
}
2025-03-16 15:10:12 +00:00
#endregion
#region Window Animation
2014-07-23 20:46:19 +00:00
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)));
2022-12-17 09:37:24 +00:00
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();
2022-11-24 20:01:32 +00:00
const string progressBarAnimationName = "ProgressBarAnimation";
var beginStoryboard = new BeginStoryboard
{
Name = progressBarAnimationName, Storyboard = progressBarStoryBoard
};
2022-11-24 20:01:32 +00:00
var stopStoryboard = new StopStoryboard()
{
BeginStoryboardName = progressBarAnimationName
};
2022-11-24 20:01:32 +00:00
var trigger = new Trigger
{
Property = VisibilityProperty, Value = Visibility.Visible
};
trigger.EnterActions.Add(beginStoryboard);
2022-11-24 20:01:32 +00:00
trigger.ExitActions.Add(stopStoryboard);
2025-03-14 11:40:03 +00:00
var progressStyle = new Style(typeof(Line))
2022-11-24 20:01:32 +00:00
{
BasedOn = FindResource("PendingLineStyle") as Style
};
progressStyle.RegisterName(progressBarAnimationName, beginStoryboard);
progressStyle.Triggers.Add(trigger);
ProgressBar.Style = progressStyle;
_viewModel.ProgressBarVisibility = Visibility.Hidden;
2014-07-23 20:46:19 +00:00
}
2022-12-23 03:46:12 +00:00
2025-03-16 15:10:12 +00:00
private void WindowAnimation()
{
2025-03-22 05:26:12 +00:00
_isArrowKeyPressed = true;
2022-11-21 17:49:10 +00:00
2025-03-17 12:09:02 +00:00
var clocksb = new Storyboard();
var iconsb = new Storyboard();
var easing = new CircleEase { EasingMode = EasingMode.EaseInOut };
2022-11-21 17:49:10 +00:00
var animationLength = _settings.AnimationSpeed switch
{
AnimationSpeeds.Slow => 560,
AnimationSpeeds.Medium => 360,
AnimationSpeeds.Fast => 160,
_ => _settings.CustomAnimationLength
};
2022-11-21 17:49:10 +00:00
var IconMotion = new DoubleAnimation
{
From = 12,
To = 0,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
2022-11-21 17:49:10 +00:00
};
var ClockOpacity = new DoubleAnimation
{
From = 0,
To = 1,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
2022-11-21 17:49:10 +00:00
};
var TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0);
2025-03-06 12:19:09 +00:00
2022-11-21 17:49:10 +00:00
var IconOpacity = new DoubleAnimation
{
From = 0,
To = TargetIconOpacity,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
2022-11-21 17:49:10 +00:00
};
var rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right;
2022-11-21 17:49:10 +00:00
var thicknessAnimation = new ThicknessAnimation
{
From = new Thickness(0, 12, rightMargin, 0),
To = new Thickness(0, 0, rightMargin, 0),
2022-11-21 17:49:10 +00:00
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(animationLength),
FillBehavior = FillBehavior.HoldEnd
2022-11-21 17:49:10 +00:00
};
Storyboard.SetTarget(ClockOpacity, ClockPanel);
2025-04-25 08:28:29 +00:00
Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty));
2025-04-25 08:28:29 +00:00
Storyboard.SetTarget(thicknessAnimation, ClockPanel);
2022-11-21 17:49:10 +00:00
Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty));
Storyboard.SetTarget(IconMotion, SearchIcon);
2022-11-21 17:49:10 +00:00
Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty));
Storyboard.SetTarget(IconOpacity, SearchIcon);
2022-11-21 17:49:10 +00:00
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;
2025-03-22 05:26:12 +00:00
_isArrowKeyPressed = false;
2025-03-26 16:14:13 +00:00
clocksb.Begin(ClockPanel);
iconsb.Begin(SearchIcon);
}
2025-03-06 11:49:14 +00:00
private void UpdateClockPanelVisibility()
{
if (QueryTextBox == null || ResultContextMenu == null || History == null || ClockPanel == null)
2025-03-28 13:42:40 +00:00
{
2025-03-06 11:49:14 +00:00
return;
2025-03-28 13:42:40 +00:00
}
2025-03-06 11:49:14 +00:00
2025-03-28 13:42:40 +00:00
// ✅ Initialize animation length & duration
2025-03-06 11:49:14 +00:00
var animationLength = _settings.AnimationSpeed switch
2024-05-19 12:13:37 +00:00
{
2025-03-06 11:49:14 +00:00
AnimationSpeeds.Slow => 560,
AnimationSpeeds.Medium => 360,
AnimationSpeeds.Fast => 160,
_ => _settings.CustomAnimationLength
};
var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3);
2025-03-06 11:49:14 +00:00
// ✅ Conditions for showing ClockPanel (No query input / ResultContextMenu & History are closed)
var shouldShowClock = QueryTextBox.Text.Length == 0 &&
ResultContextMenu.Visibility != Visibility.Visible &&
2025-03-17 12:41:06 +00:00
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;
}
2025-03-06 11:49:14 +00:00
// ✅ 2. When ResultContextMenu is closed, keep it Hidden if there's text in the query (remember previous state)
2025-03-28 13:42:40 +00:00
else if (QueryTextBox.Text.Length > 0)
{
_viewModel.ClockPanelVisibility = Visibility.Hidden;
_viewModel.ClockPanelOpacity = 0.0;
return;
}
2025-03-28 13:42:40 +00:00
// ✅ Prevent multiple animations
if (_isClockPanelAnimating)
2024-05-19 12:13:37 +00:00
{
return;
2024-05-19 12:13:37 +00:00
}
2025-03-18 15:09:02 +00:00
// ✅ 3. When hiding ClockPanel (apply fade-out animation)
2025-03-28 13:42:40 +00:00
if ((!shouldShowClock) && _viewModel.ClockPanelVisibility == Visibility.Visible)
{
2025-03-06 11:49:14 +00:00
_isClockPanelAnimating = true;
2025-03-06 11:49:14 +00:00
var fadeOut = new DoubleAnimation
{
From = 1.0,
To = 0.0,
Duration = animationDuration,
FillBehavior = FillBehavior.HoldEnd
2025-03-06 11:49:14 +00:00
};
2025-03-06 11:49:14 +00:00
fadeOut.Completed += (s, e) =>
{
_viewModel.ClockPanelVisibility = Visibility.Hidden; // ✅ Completely hide after animation
2025-03-06 11:49:14 +00:00
_isClockPanelAnimating = false;
};
2025-03-16 07:05:15 +00:00
ClockPanel.BeginAnimation(OpacityProperty, fadeOut);
2025-03-06 11:49:14 +00:00
}
2025-03-28 05:50:36 +00:00
2025-03-18 15:09:02 +00:00
// ✅ 4. When showing ClockPanel (apply fade-in animation)
2025-03-28 13:42:40 +00:00
else if (shouldShowClock && _viewModel.ClockPanelVisibility != Visibility.Visible)
{
2025-03-06 11:49:14 +00:00
_isClockPanelAnimating = true;
2025-03-28 13:42:40 +00:00
_viewModel.ClockPanelVisibility = Visibility.Visible; // ✅ Set Visibility to Visible first
var fadeIn = new DoubleAnimation
{
2025-03-28 13:42:40 +00:00
From = 0.0,
To = 1.0,
Duration = animationDuration,
FillBehavior = FillBehavior.HoldEnd
};
2025-03-28 13:42:40 +00:00
fadeIn.Completed += (s, e) => _isClockPanelAnimating = false;
ClockPanel.BeginAnimation(OpacityProperty, fadeIn);
}
}
2025-03-16 15:10:12 +00:00
private static double GetOpacityFromStyle(Style style, double defaultOpacity = 1.0)
{
2025-03-16 15:10:12 +00:00
if (style == null)
2025-03-28 13:42:40 +00:00
{
2025-03-16 15:10:12 +00:00
return defaultOpacity;
2025-03-28 13:42:40 +00:00
}
2024-05-19 12:13:37 +00:00
2025-03-16 15:10:12 +00:00
foreach (Setter setter in style.Setters.Cast<Setter>())
{
2025-03-16 15:10:12 +00:00
if (setter.Property == OpacityProperty)
{
2025-03-16 15:10:12 +00:00
return setter.Value is double opacity ? opacity : defaultOpacity;
}
}
2025-03-16 15:10:12 +00:00
return defaultOpacity;
}
2025-03-16 15:10:12 +00:00
private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness)
{
2025-03-16 15:10:12 +00:00
if (style == null)
2025-03-28 13:42:40 +00:00
{
2025-03-16 15:10:12 +00:00
return defaultThickness;
2025-03-28 13:42:40 +00:00
}
2025-03-16 15:10:12 +00:00
foreach (Setter setter in style.Setters.Cast<Setter>())
{
2025-03-16 15:10:12 +00:00
if (setter.Property == MarginProperty)
{
2025-03-16 15:10:12 +00:00
return setter.Value is Thickness thickness ? thickness : defaultThickness;
}
}
2025-03-16 15:10:12 +00:00
return defaultThickness;
}
2025-03-16 15:10:12 +00:00
#endregion
2025-03-16 15:10:12 +00:00
#region QueryTextBox Event
2025-03-16 15:10:12 +00:00
private void QueryTextBox_OnCopy(object sender, ExecutedRoutedEventArgs e)
2024-05-16 08:04:29 +00:00
{
2025-03-16 15:10:12 +00:00
var result = _viewModel.Results.SelectedItem?.Result;
if (QueryTextBox.SelectionLength == 0 && result != null)
2024-05-16 08:04:29 +00:00
{
2025-03-16 15:10:12 +00:00
string copyText = result.CopyText;
App.API.CopyToClipboard(copyText, directCopy: true);
2024-05-16 08:04:29 +00:00
}
2025-03-16 15:10:12 +00:00
else if (!string.IsNullOrEmpty(QueryTextBox.Text))
2024-05-16 08:04:29 +00:00
{
2025-03-16 15:10:12 +00:00
App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false);
2024-05-16 08:04:29 +00:00
}
}
2025-03-16 15:10:12 +00:00
private void QueryTextBox_OnPaste(object sender, DataObjectPastingEventArgs e)
{
2025-07-11 14:24:05 +00:00
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)
{
2025-07-11 14:24:05 +00:00
App.API.LogException(ClassName, "Failed to paste text", ex);
}
}
2022-09-14 08:50:42 +00:00
private void QueryTextBox_KeyUp(object sender, KeyEventArgs e)
{
if (_viewModel.QueryText != QueryTextBox.Text)
2022-09-21 11:13:50 +00:00
{
2025-03-16 14:37:22 +00:00
BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty);
2022-09-21 11:13:50 +00:00
be.UpdateSource();
}
2022-09-14 08:50:42 +00:00
}
2025-03-30 04:20:14 +00:00
2025-03-16 15:10:12 +00:00
private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e)
{
2025-03-16 15:10:12 +00:00
e.Handled = true;
}
2025-03-16 15:10:12 +00:00
#endregion
2025-03-29 05:10:06 +00:00
#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
2025-03-29 05:25:00 +00:00
#region Resize Mode
private void SetupResizeMode()
{
ResizeMode = _settings.KeepMaxResults ? ResizeMode.NoResize : ResizeMode.CanResize;
if (WindowChrome.GetWindowChrome(this) is WindowChrome windowChrome)
{
2025-03-29 08:41:11 +00:00
_theme.SetResizeBorderThickness(windowChrome, _settings.KeepMaxResults);
}
2025-03-29 05:25:00 +00:00
}
2025-03-20 23:42:04 +00:00
#endregion
2025-03-15 13:58:55 +00:00
#region Search Delay
2025-03-30 01:38:59 +00:00
private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e)
2025-03-15 13:58:55 +00:00
{
var textBox = (TextBox)sender;
2025-03-30 14:40:46 +00:00
_viewModel.QueryText = textBox.Text;
_viewModel.Query(_settings.SearchQueryResultsWithDelay);
2025-03-15 13:58:55 +00:00
}
#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)
{
2025-03-22 05:26:12 +00:00
if (!_disposed)
{
if (disposing)
{
_hwndSource?.Dispose();
2025-03-22 05:26:12 +00:00
_notifyIcon?.Dispose();
2025-09-30 11:41:27 +00:00
_animationSoundWMP?.Close();
_animationSoundWPF?.Dispose();
_viewModel.ActualApplicationThemeChanged -= ViewModel_ActualApplicationThemeChanged;
UnregisterSoundEffectsEvent();
}
2025-03-22 05:26:12 +00:00
_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
2014-07-23 20:46:19 +00:00
}
2022-09-14 08:50:42 +00:00
}