Merge branch 'dev' into 050328-NewHotkeyDesign

This commit is contained in:
DB p 2025-03-29 01:18:34 +09:00
commit 8d8719aa12
3 changed files with 123 additions and 95 deletions

View file

@ -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 @@
<StackPanel
x:Name="ClockPanel"
IsHitTestVisible="False"
Style="{DynamicResource ClockPanel}">
Opacity="{Binding ClockPanelOpacity}"
Style="{DynamicResource ClockPanel}"
Visibility="{Binding ClockPanelVisibility}">
<TextBlock
x:Name="ClockBox"
Style="{DynamicResource ClockBox}"
@ -318,6 +319,7 @@
Name="SearchIcon"
Margin="0"
Data="{DynamicResource SearchIconImg}"
Opacity="{Binding SearchIconOpacity}"
Stretch="Fill"
Style="{DynamicResource SearchIconStyle}"
Visibility="{Binding SearchIconVisibility}" />

View file

@ -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<Setter>())
{
@ -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<Setter>())
{

View file

@ -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
/// </summary>
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 });