diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 087b2e998..533819d17 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -24,7 +24,6 @@ Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Loaded="OnLoaded" LocationChanged="OnLocationChanged" - Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="OnKeyDown" PreviewKeyUp="OnKeyUp" PreviewMouseMove="OnPreviewMouseMove" @@ -290,7 +289,9 @@ + Opacity="{Binding ClockPanelOpacity}" + Style="{DynamicResource ClockPanel}" + Visibility="{Binding ClockPanelVisibility}"> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 25d9c5d2d..0fd802f1e 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -101,13 +101,18 @@ namespace Flow.Launcher // Check first launch if (_settings.FirstLaunch) { + // Set First Launch to false _settings.FirstLaunch = false; + + // 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(); - /* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */ - if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) - _settings.BackdropType = BackdropTypes.Acrylic; - var WelcomeWindow = new WelcomeWindow(); - WelcomeWindow.Show(); + + // Show Welcome Window + var welcomeWindow = new WelcomeWindow(); + welcomeWindow.Show(); } // Hide window if need @@ -161,24 +166,37 @@ namespace Flow.Launcher { if (_viewModel.MainWindowVisibilityStatus) { + // Play sound effect before activing the window if (_settings.UseSound) { SoundPlay(); } + // Update position & Activate UpdatePosition(); - _viewModel.ResetPreview(); Activate(); - QueryTextBox.Focus(); - _settings.ActivateTimes++; + + // 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) + { WindowAnimation(); + } + + // Update activate times + _settings.ActivateTimes++; } }); break; @@ -191,7 +209,6 @@ namespace Flow.Launcher Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); _viewModel.QueryTextCursorMovedToEnd = false; } - break; case nameof(MainViewModel.GameModeStatus): _notifyIcon.Icon = _viewModel.GameModeStatus @@ -224,7 +241,7 @@ namespace Flow.Launcher }; // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher) - QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); + QueryTextBox.TextChanged += (s, e) => UpdateClockPanelVisibility(); // Detecting ContextMenu.Visibility changes DependencyPropertyDescriptor @@ -248,7 +265,8 @@ namespace Flow.Launcher Notification.Uninstall(); // After plugins are all disposed, we can close the main window _canClose = true; - Close(); + // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event + Application.Current.Shutdown(); } } @@ -280,8 +298,8 @@ namespace Flow.Launcher _settings.WindowLeft = Left; _settings.WindowTop = Top; - ClockPanel.Opacity = 0; - SearchIcon.Opacity = 0; + _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. @@ -291,7 +309,9 @@ namespace Flow.Launcher // 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) { @@ -331,7 +351,6 @@ namespace Flow.Launcher _viewModel.LoadContextMenuCommand.Execute(null); e.Handled = true; } - break; case Key.Left: if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0) @@ -339,7 +358,6 @@ namespace Flow.Launcher _viewModel.EscCommand.Execute(null); e.Handled = true; } - break; case Key.Back: if (specialKeyState.CtrlPressed) @@ -358,7 +376,6 @@ namespace Flow.Launcher } } } - break; default: break; @@ -765,12 +782,6 @@ namespace Flow.Launcher { _isArrowKeyPressed = true; - UpdatePosition(); - - var opacity = _settings.UseAnimation ? 0.0 : 1.0; - ClockPanel.Opacity = opacity; - SearchIcon.Opacity = opacity; - var clocksb = new Storyboard(); var iconsb = new Storyboard(); var easing = new CircleEase { EasingMode = EasingMode.EaseInOut }; @@ -850,8 +861,11 @@ namespace Flow.Launcher private void UpdateClockPanelVisibility() { if (QueryTextBox == null || ContextMenu == null || History == null || ClockPanel == null) + { return; + } + // ✅ Initialize animation length & duration var animationLength = _settings.AnimationSpeed switch { AnimationSpeeds.Slow => 560, @@ -859,32 +873,37 @@ namespace Flow.Launcher AnimationSpeeds.Fast => 160, _ => _settings.CustomAnimationLength }; - var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3); // ✅ Conditions for showing ClockPanel (No query input & ContextMenu, History are closed) - bool shouldShowClock = QueryTextBox.Text.Length == 0 && + var shouldShowClock = QueryTextBox.Text.Length == 0 && ContextMenu.Visibility != Visibility.Visible && History.Visibility != Visibility.Visible; // ✅ 1. When ContextMenu opens, immediately set Visibility.Hidden (force hide without animation) if (ContextMenu.Visibility == Visibility.Visible) { - ClockPanel.Visibility = Visibility.Hidden; - ClockPanel.Opacity = 0.0; // Set to 0 in case Opacity animation affects it + _viewModel.ClockPanelVisibility = Visibility.Hidden; + _viewModel.ClockPanelOpacity = 0.0; // Set to 0 in case Opacity animation affects it return; } // ✅ 2. When ContextMenu is closed, keep it Hidden if there's text in the query (remember previous state) - if (ContextMenu.Visibility != Visibility.Visible && QueryTextBox.Text.Length > 0) + else if (QueryTextBox.Text.Length > 0) + { + _viewModel.ClockPanelVisibility = Visibility.Hidden; + _viewModel.ClockPanelOpacity = 0.0; + return; + } + + // ✅ Prevent multiple animations + if (_isClockPanelAnimating) { - ClockPanel.Visibility = Visibility.Hidden; - ClockPanel.Opacity = 0.0; return; } // ✅ 3. When hiding ClockPanel (apply fade-out animation) - if ((!shouldShowClock) && ClockPanel.Visibility == Visibility.Visible && !_isClockPanelAnimating) + if ((!shouldShowClock) && _viewModel.ClockPanelVisibility == Visibility.Visible) { _isClockPanelAnimating = true; @@ -898,40 +917,40 @@ namespace Flow.Launcher fadeOut.Completed += (s, e) => { - ClockPanel.Visibility = Visibility.Hidden; // ✅ Completely hide after animation + _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 && ClockPanel.Visibility != Visibility.Visible && !_isClockPanelAnimating) + else if (shouldShowClock && _viewModel.ClockPanelVisibility != Visibility.Visible) { _isClockPanelAnimating = true; - Application.Current.Dispatcher.Invoke(() => + _viewModel.ClockPanelVisibility = Visibility.Visible; // ✅ Set Visibility to Visible first + + var fadeIn = new DoubleAnimation { - ClockPanel.Visibility = Visibility.Visible; // ✅ Set Visibility to Visible first + From = 0.0, + To = 1.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; - var fadeIn = new DoubleAnimation - { - From = 0.0, - To = 1.0, - Duration = animationDuration, - FillBehavior = FillBehavior.HoldEnd - }; + fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; - fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; - ClockPanel.BeginAnimation(OpacityProperty, fadeIn); - }, DispatcherPriority.Render); + 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()) { @@ -947,7 +966,9 @@ namespace Flow.Launcher private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness) { if (style == null) + { return defaultThickness; + } foreach (Setter setter in style.Setters.Cast()) { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 2bfc9587c..c69fc4484 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -213,7 +213,8 @@ namespace Flow.Launcher.ViewModel queue.Clear(); } - Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); + if (!_disposed) + Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); } void continueAction(Task t) @@ -638,25 +639,29 @@ namespace Flow.Launcher.ViewModel /// private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false) { - await Application.Current.Dispatcher.InvokeAsync(async () => + // Must check access so that we will not block the UI thread which cause window visibility issue + if (!Application.Current.Dispatcher.CheckAccess()) { - BackToQueryResults(); + await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery)); + return; + } - if (QueryText != queryText) - { - // re-query is done in QueryText's setter method - QueryText = queryText; - // set to false so the subsequent set true triggers - // PropertyChanged and MoveQueryTextToEnd is called - QueryTextCursorMovedToEnd = false; - } - else if (isReQuery) - { - await QueryAsync(isReQuery: true); - } + BackToQueryResults(); - QueryTextCursorMovedToEnd = true; - }); + if (QueryText != queryText) + { + // re-query is done in QueryText's setter method + QueryText = queryText; + // set to false so the subsequent set true triggers + // PropertyChanged and MoveQueryTextToEnd is called + QueryTextCursorMovedToEnd = false; + } + else if (isReQuery) + { + await QueryAsync(isReQuery: true); + } + + QueryTextCursorMovedToEnd = true; } public bool LastQuerySelected { get; set; } @@ -748,15 +753,17 @@ namespace Flow.Launcher.ViewModel public Visibility ProgressBarVisibility { get; set; } public Visibility MainWindowVisibility { get; set; } - public double MainWindowOpacity { get; set; } = 1; - + // This is to be used for determining the visibility status of the mainwindow instead of MainWindowVisibility // because it is more accurate and reliable representation than using Visibility as a condition check public bool MainWindowVisibilityStatus { get; set; } = true; public event VisibilityChangedEventHandler VisibilityChanged; + public Visibility ClockPanelVisibility { get; set; } public Visibility SearchIconVisibility { get; set; } + public double ClockPanelOpacity { get; set; } = 1; + public double SearchIconOpacity { get; set; } = 1; public double MainWindowWidth { @@ -1444,50 +1451,41 @@ namespace Flow.Launcher.ViewModel #pragma warning disable VSTHRD100 // Avoid async void methods - public async void Show() + public void Show() { - await Application.Current.Dispatcher.InvokeAsync(() => + // Invoke on UI thread + Application.Current.Dispatcher.Invoke(() => { - if (Application.Current.MainWindow is MainWindow mainWindow) + // When application is exitting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) { // 📌 Remove DWM Cloak (Make the window visible normally) Win32Helper.DWMSetCloakForWindow(mainWindow, false); - // Clock and SearchIcon hide when show situation + // Set clock and search icon opacity var opacity = Settings.UseAnimation ? 0.0 : 1.0; - mainWindow.ClockPanel.Opacity = opacity; - mainWindow.SearchIcon.Opacity = opacity; - - // QueryText sometimes is null when it is just initialized - if (QueryText != null && QueryText.Length != 0) - { - mainWindow.ClockPanel.Visibility = Visibility.Collapsed; - } - else - { - mainWindow.ClockPanel.Visibility = Visibility.Visible; - } + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + // Set clock and search icon visibility + ClockPanelVisibility = string.IsNullOrEmpty(QueryText) ? Visibility.Visible : Visibility.Collapsed; if (PluginIconSource != null) { - mainWindow.SearchIcon.Opacity = 0; + SearchIconOpacity = 0.0; } else { SearchIconVisibility = Visibility.Visible; } - - // 📌 Restore UI elements - //mainWindow.SearchIcon.Visibility = Visibility.Visible; } }, DispatcherPriority.Render); // Update WPF properties MainWindowVisibility = Visibility.Visible; - MainWindowOpacity = 1; MainWindowVisibilityStatus = true; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); + // Switch keyboard layout if (StartWithEnglishMode) { Win32Helper.SwitchToEnglishKeyboardLayout(true); @@ -1530,15 +1528,19 @@ namespace Flow.Launcher.ViewModel break; } - await Application.Current.Dispatcher.InvokeAsync(() => + // Invoke on UI thread + Application.Current.Dispatcher.Invoke(() => { - if (Application.Current.MainWindow is MainWindow mainWindow) + // When application is exitting, the Application.Current will be null + if (Application.Current?.MainWindow is MainWindow mainWindow) { - // 📌 Set Opacity of icon and clock to 0 and apply Visibility.Hidden + // Set clock and search icon opacity var opacity = Settings.UseAnimation ? 0.0 : 1.0; - mainWindow.ClockPanel.Opacity = opacity; - mainWindow.SearchIcon.Opacity = opacity; - mainWindow.ClockPanel.Visibility = Visibility.Hidden; + ClockPanelOpacity = opacity; + SearchIconOpacity = opacity; + + // Set clock and search icon visibility + ClockPanelVisibility = Visibility.Hidden; SearchIconVisibility = Visibility.Hidden; // Force UI update @@ -1548,15 +1550,18 @@ namespace Flow.Launcher.ViewModel // 📌 Apply DWM Cloak (Completely hide the window) Win32Helper.DWMSetCloakForWindow(mainWindow, true); } - }); + }, DispatcherPriority.Render); + // Switch keyboard layout if (StartWithEnglishMode) { Win32Helper.RestorePreviousKeyboardLayout(); } + // Delay for a while to make sure clock will not flicker + await Task.Delay(50); + // Update WPF properties - //MainWindowOpacity = 0; MainWindowVisibilityStatus = false; MainWindowVisibility = Visibility.Collapsed; VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false });