Merge pull request #3370 from Jack251970/history_preview1

Support Preview Panel for History Results
This commit is contained in:
Jack Ye 2025-03-26 12:26:41 +08:00 committed by GitHub
commit 350276dbf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 193 additions and 122 deletions

View file

@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png"); public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png"); public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png"); public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
public static string PythonPath; public static string PythonPath;
public static string NodePath; public static string NodePath;

View file

@ -216,7 +216,7 @@
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}"> <Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<Grid> <Grid x:Name="QueryBoxArea">
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}"> <Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
<Grid> <Grid>
<TextBox <TextBox
@ -339,7 +339,7 @@
Y2="0" /> Y2="0" />
</Grid> </Grid>
<Grid ClipToBounds="True"> <Grid x:Name="MiddleSeparatorArea" ClipToBounds="True">
<ContentControl> <ContentControl>
<ContentControl.Style> <ContentControl.Style>
<Style TargetType="ContentControl"> <Style TargetType="ContentControl">
@ -379,7 +379,7 @@
</ContentControl> </ContentControl>
</Grid> </Grid>
<Border Style="{DynamicResource WindowRadius}"> <Border x:Name="ResultPreviewAreaBoarder" Style="{DynamicResource WindowRadius}">
<Border.Clip> <Border.Clip>
<MultiBinding Converter="{StaticResource BorderClipConverter}"> <MultiBinding Converter="{StaticResource BorderClipConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" /> <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
@ -387,12 +387,14 @@
<Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}" /> <Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}" />
</MultiBinding> </MultiBinding>
</Border.Clip> </Border.Clip>
<Grid>
<Grid x:Name="ResultPreviewArea">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="80" /> <ColumnDefinition Width="*" MinWidth="80" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="0.85*" MinWidth="244" /> <ColumnDefinition Width="0.85*" MinWidth="244" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel <StackPanel
x:Name="ResultArea" x:Name="ResultArea"
Grid.Column="0" Grid.Column="0"
@ -419,7 +421,9 @@
RightClickResultCommand="{Binding RightClickResultCommand}" /> RightClickResultCommand="{Binding RightClickResultCommand}" />
</ContentControl> </ContentControl>
</StackPanel> </StackPanel>
<GridSplitter <GridSplitter
x:Name="PreviewMiddleSeparator"
Grid.Column="1" Grid.Column="1"
Margin="0" Margin="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@ -433,6 +437,7 @@
</ControlTemplate> </ControlTemplate>
</GridSplitter.Template> </GridSplitter.Template>
</GridSplitter> </GridSplitter>
<Grid <Grid
x:Name="Preview" x:Name="Preview"
Grid.Column="2" Grid.Column="2"
@ -442,7 +447,7 @@
<Border <Border
MinHeight="380" MinHeight="380"
d:DataContext="{d:DesignInstance vm:ResultViewModel}" d:DataContext="{d:DesignInstance vm:ResultViewModel}"
DataContext="{Binding SelectedItem, ElementName=ResultListBox}" DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
Visibility="{Binding ShowDefaultPreview}"> Visibility="{Binding ShowDefaultPreview}">
<Grid <Grid
Margin="0 0 10 5" Margin="0 0 10 5"
@ -519,7 +524,7 @@
MaxHeight="{Binding ElementName=ResultListBox, Path=ActualHeight}" MaxHeight="{Binding ElementName=ResultListBox, Path=ActualHeight}"
Padding="0 0 10 10" Padding="0 0 10 10"
d:DataContext="{d:DesignInstance vm:ResultViewModel}" d:DataContext="{d:DesignInstance vm:ResultViewModel}"
DataContext="{Binding SelectedItem, ElementName=ResultListBox}" DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
Visibility="{Binding ShowCustomizedPreview}"> Visibility="{Binding ShowCustomizedPreview}">
<ContentControl Content="{Binding Result.PreviewPanel.Value}" /> <ContentControl Content="{Binding Result.PreviewPanel.Value}" />
</Border> </Border>

View file

@ -326,7 +326,7 @@ namespace Flow.Launcher
e.Handled = true; e.Handled = true;
break; break;
case Key.Right: case Key.Right:
if (_viewModel.SelectedIsFromQueryResults() if (_viewModel.QueryResultsSelected()
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length && QueryTextBox.CaretIndex == QueryTextBox.Text.Length
&& !string.IsNullOrEmpty(QueryTextBox.Text)) && !string.IsNullOrEmpty(QueryTextBox.Text))
{ {
@ -336,7 +336,7 @@ namespace Flow.Launcher
break; break;
case Key.Left: case Key.Left:
if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0) if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
{ {
_viewModel.EscCommand.Execute(null); _viewModel.EscCommand.Execute(null);
e.Handled = true; e.Handled = true;
@ -346,7 +346,7 @@ namespace Flow.Launcher
case Key.Back: case Key.Back:
if (specialKeyState.CtrlPressed) if (specialKeyState.CtrlPressed)
{ {
if (_viewModel.SelectedIsFromQueryResults() if (_viewModel.QueryResultsSelected()
&& QueryTextBox.Text.Length > 0 && QueryTextBox.Text.Length > 0
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{ {

View file

@ -390,6 +390,7 @@
<Style.Triggers> <Style.Triggers>
<MultiDataTrigger> <MultiDataTrigger>
<MultiDataTrigger.Conditions> <MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" /> <Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
</MultiDataTrigger.Conditions> </MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters> <MultiDataTrigger.Setters>
@ -431,12 +432,12 @@
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<Style <Style
x:Key="PreviewBorderStyle" x:Key="PreviewBorderStyle"
BasedOn="{StaticResource BasePreviewBorderStyle}" BasedOn="{StaticResource BasePreviewBorderStyle}"
TargetType="{x:Type Border}"> TargetType="{x:Type Border}">
<Setter Property="BorderBrush" Value="Gray" /> <Setter Property="BorderBrush" Value="Gray" />
</Style> </Style>
<Style x:Key="PreviewArea" TargetType="{x:Type Grid}"> <Style x:Key="PreviewArea" TargetType="{x:Type Grid}">
@ -446,8 +447,8 @@
<MultiDataTrigger.Conditions> <MultiDataTrigger.Conditions>
<!-- <!--
<Condition Binding="{Binding ElementName=ResultListBox, Path=Visibility}" Value="Collapsed" /> <Condition Binding="{Binding ElementName=ResultListBox, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" /> <Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />-->
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />--> <Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" /> <Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
</MultiDataTrigger.Conditions> </MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters> <MultiDataTrigger.Setters>

View file

@ -49,6 +49,8 @@ namespace Flow.Launcher.ViewModel
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter; private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
private Task _resultsViewUpdateTask; private Task _resultsViewUpdateTask;
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
#endregion #endregion
#region Constructor #region Constructor
@ -162,7 +164,21 @@ namespace Flow.Launcher.ViewModel
switch (args.PropertyName) switch (args.PropertyName)
{ {
case nameof(Results.SelectedItem): case nameof(Results.SelectedItem):
UpdatePreview(); _selectedItemFromQueryResults = true;
PreviewSelectedItem = Results.SelectedItem;
_ = UpdatePreviewAsync();
break;
}
};
History.PropertyChanged += (_, args) =>
{
switch (args.PropertyName)
{
case nameof(History.SelectedItem):
_selectedItemFromQueryResults = false;
PreviewSelectedItem = History.SelectedItem;
_ = UpdatePreviewAsync();
break; break;
} }
}; };
@ -265,7 +281,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void LoadHistory() private void LoadHistory()
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
SelectedResults = History; SelectedResults = History;
History.SelectedIndex = _history.Items.Count - 1; History.SelectedIndex = _history.Items.Count - 1;
@ -279,7 +295,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
public void ReQuery() public void ReQuery()
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
_ = QueryResultsAsync(isReQuery: true); _ = QueryResultsAsync(isReQuery: true);
} }
@ -320,7 +336,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void LoadContextMenu() private void LoadContextMenu()
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
@ -350,7 +366,7 @@ namespace Flow.Launcher.ViewModel
private void AutocompleteQuery() private void AutocompleteQuery()
{ {
var result = SelectedResults.SelectedItem?.Result; var result = SelectedResults.SelectedItem?.Result;
if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty. if (result != null && QueryResultsSelected()) // SelectedItem returns null if selection is empty.
{ {
var autoCompleteText = result.Title; var autoCompleteText = result.Title;
@ -402,7 +418,7 @@ namespace Flow.Launcher.ViewModel
}) })
.ConfigureAwait(false); .ConfigureAwait(false);
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
_userSelectedRecord.Add(result); _userSelectedRecord.Add(result);
// origin query is null when user select the context menu item directly of one item from query list // origin query is null when user select the context menu item directly of one item from query list
@ -481,7 +497,7 @@ namespace Flow.Launcher.ViewModel
{ {
if (_history.Items.Count > 0 if (_history.Items.Count > 0
&& QueryText == string.Empty && QueryText == string.Empty
&& SelectedIsFromQueryResults()) && QueryResultsSelected())
{ {
lastHistoryIndex = 1; lastHistoryIndex = 1;
ReverseHistory(); ReverseHistory();
@ -501,7 +517,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void Esc() private void Esc()
{ {
if (!SelectedIsFromQueryResults()) if (!QueryResultsSelected())
{ {
SelectedResults = Results; SelectedResults = Results;
} }
@ -513,7 +529,7 @@ namespace Flow.Launcher.ViewModel
public void BackToQueryResults() public void BackToQueryResults()
{ {
if (!SelectedIsFromQueryResults()) if (!QueryResultsSelected())
{ {
SelectedResults = Results; SelectedResults = Results;
} }
@ -644,13 +660,16 @@ namespace Flow.Launcher.ViewModel
private ResultsViewModel SelectedResults private ResultsViewModel SelectedResults
{ {
get { return _selectedResults; } get => _selectedResults;
set set
{ {
var isReturningFromQueryResults = QueryResultsSelected();
var isReturningFromContextMenu = ContextMenuSelected(); var isReturningFromContextMenu = ContextMenuSelected();
var isReturningFromHistory = HistorySelected();
_selectedResults = value; _selectedResults = value;
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
Results.Visibility = Visibility.Visible;
ContextMenu.Visibility = Visibility.Collapsed; ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Collapsed; History.Visibility = Visibility.Collapsed;
@ -668,10 +687,27 @@ namespace Flow.Launcher.ViewModel
{ {
ChangeQueryText(_queryTextBeforeLeaveResults); ChangeQueryText(_queryTextBeforeLeaveResults);
} }
// If we are returning from history and we have not set select item yet,
// we need to clear the preview selected item
if (isReturningFromHistory && _selectedItemFromQueryResults.HasValue && (!_selectedItemFromQueryResults.Value))
{
PreviewSelectedItem = null;
}
} }
else else
{ {
Results.Visibility = Visibility.Collapsed; Results.Visibility = Visibility.Collapsed;
if (HistorySelected())
{
ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Visible;
}
else
{
ContextMenu.Visibility = Visibility.Visible;
History.Visibility = Visibility.Collapsed;
}
_queryTextBeforeLeaveResults = QueryText; _queryTextBeforeLeaveResults = QueryText;
// Because of Fody's optimization // Because of Fody's optimization
@ -686,6 +722,16 @@ namespace Flow.Launcher.ViewModel
{ {
QueryText = string.Empty; QueryText = string.Empty;
} }
if (HistorySelected())
{
// If we are returning from query results and we have not set select item yet,
// we need to clear the preview selected item
if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value)
{
PreviewSelectedItem = null;
}
}
} }
_selectedResults.Visibility = Visibility.Visible; _selectedResults.Visibility = Visibility.Visible;
@ -785,6 +831,22 @@ namespace Flow.Launcher.ViewModel
#region Preview #region Preview
private static readonly int ResultAreaColumnPreviewShown = 1;
private static readonly int ResultAreaColumnPreviewHidden = 3;
private bool? _selectedItemFromQueryResults;
private ResultViewModel _previewSelectedItem;
public ResultViewModel PreviewSelectedItem
{
get => _previewSelectedItem;
set
{
_previewSelectedItem = value;
OnPropertyChanged();
}
}
public bool InternalPreviewVisible public bool InternalPreviewVisible
{ {
get get
@ -803,18 +865,14 @@ namespace Flow.Launcher.ViewModel
} }
} }
private static readonly int ResultAreaColumnPreviewShown = 1;
private static readonly int ResultAreaColumnPreviewHidden = 3;
public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown; public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
// This is not a reliable indicator of whether external preview is visible due to the // This is not a reliable indicator of whether external preview is visible due to the
// ability of manually closing/exiting the external preview program which, does not inform flow that // ability of manually closing/exiting the external preview program which, does not inform flow that
// preview is no longer available. // preview is no longer available.
public bool ExternalPreviewVisible { get; set; } = false; public bool ExternalPreviewVisible { get; private set; }
private void ShowPreview() private async Task ShowPreviewAsync()
{ {
var useExternalPreview = PluginManager.UseExternalPreview(); var useExternalPreview = PluginManager.UseExternalPreview();
@ -825,13 +883,15 @@ namespace Flow.Launcher.ViewModel
// Internal preview may still be on when user switches to external // Internal preview may still be on when user switches to external
if (InternalPreviewVisible) if (InternalPreviewVisible)
HideInternalPreview(); HideInternalPreview();
OpenExternalPreview(path);
_ = OpenExternalPreviewAsync(path);
break; break;
case true case true
when !CanExternalPreviewSelectedResult(out var _): when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
CloseExternalPreview(); await CloseExternalPreviewAsync();
ShowInternalPreview(); ShowInternalPreview();
break; break;
@ -844,7 +904,7 @@ namespace Flow.Launcher.ViewModel
private void HidePreview() private void HidePreview()
{ {
if (PluginManager.UseExternalPreview()) if (PluginManager.UseExternalPreview())
CloseExternalPreview(); _ = CloseExternalPreviewAsync();
if (InternalPreviewVisible) if (InternalPreviewVisible)
HideInternalPreview(); HideInternalPreview();
@ -859,31 +919,31 @@ namespace Flow.Launcher.ViewModel
} }
else else
{ {
ShowPreview(); _ = ShowPreviewAsync();
} }
} }
private void OpenExternalPreview(string path, bool sendFailToast = true) private async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{ {
_ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false); await PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
ExternalPreviewVisible = true; ExternalPreviewVisible = true;
} }
private void CloseExternalPreview() private async Task CloseExternalPreviewAsync()
{ {
_ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false); await PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
ExternalPreviewVisible = false; ExternalPreviewVisible = false;
} }
private static void SwitchExternalPreview(string path, bool sendFailToast = true) private static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
{ {
_ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false); await PluginManager.SwitchExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
} }
private void ShowInternalPreview() private void ShowInternalPreview()
{ {
ResultAreaColumn = ResultAreaColumnPreviewShown; ResultAreaColumn = ResultAreaColumnPreviewShown;
Results.SelectedItem?.LoadPreviewImage(); PreviewSelectedItem?.LoadPreviewImage();
} }
private void HideInternalPreview() private void HideInternalPreview()
@ -897,20 +957,18 @@ namespace Flow.Launcher.ViewModel
{ {
case true case true
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path): when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
OpenExternalPreview(path); _ = OpenExternalPreviewAsync(path);
break; break;
case true: case true:
ShowInternalPreview(); ShowInternalPreview();
break; break;
case false: case false:
HidePreview(); HidePreview();
break; break;
} }
} }
private void UpdatePreview() private async Task UpdatePreviewAsync()
{ {
switch (PluginManager.UseExternalPreview()) switch (PluginManager.UseExternalPreview())
{ {
@ -918,44 +976,48 @@ namespace Flow.Launcher.ViewModel
when CanExternalPreviewSelectedResult(out var path): when CanExternalPreviewSelectedResult(out var path):
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
{ {
SwitchExternalPreview(path, false); _ = SwitchExternalPreviewAsync(path, false);
} }
else if (InternalPreviewVisible) else if (InternalPreviewVisible)
{ {
HideInternalPreview(); HideInternalPreview();
OpenExternalPreview(path); _ = OpenExternalPreviewAsync(path);
} }
break; break;
case true case true
when !CanExternalPreviewSelectedResult(out var _): when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
{ {
CloseExternalPreview(); await CloseExternalPreviewAsync();
ShowInternalPreview(); ShowInternalPreview();
} }
break; break;
case false case false
when InternalPreviewVisible: when InternalPreviewVisible:
Results.SelectedItem?.LoadPreviewImage(); PreviewSelectedItem?.LoadPreviewImage();
break; break;
} }
} }
private bool CanExternalPreviewSelectedResult(out string path) private bool CanExternalPreviewSelectedResult(out string path)
{ {
path = Results.SelectedItem?.Result?.Preview.FilePath; path = QueryResultsPreviewed() ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty;
return !string.IsNullOrEmpty(path); return !string.IsNullOrEmpty(path);
} }
private bool QueryResultsPreviewed()
{
var previewed = PreviewSelectedItem == Results.SelectedItem;
return previewed;
}
#endregion #endregion
#region Query #region Query
public void Query(bool isReQuery = false) private void Query(bool isReQuery = false)
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
_ = QueryResultsAsync(isReQuery); _ = QueryResultsAsync(isReQuery);
} }
@ -1027,6 +1089,11 @@ namespace Flow.Launcher.ViewModel
Title = string.Format(title, h.Query), Title = string.Format(title, h.Query),
SubTitle = string.Format(time, h.ExecutedDateTime), SubTitle = string.Format(time, h.ExecutedDateTime),
IcoPath = "Images\\history.png", IcoPath = "Images\\history.png",
Preview = new Result.PreviewInfo
{
PreviewImagePath = Constant.HistoryIcon,
Description = string.Format(time, h.ExecutedDateTime)
},
OriginQuery = new Query { RawQuery = h.Query }, OriginQuery = new Query { RawQuery = h.Query },
Action = _ => Action = _ =>
{ {
@ -1053,8 +1120,6 @@ namespace Flow.Launcher.ViewModel
} }
} }
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = true) private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = true)
{ {
_updateSource?.Cancel(); _updateSource?.Cancel();
@ -1318,7 +1383,7 @@ namespace Flow.Launcher.ViewModel
return menu; return menu;
} }
internal bool SelectedIsFromQueryResults() internal bool QueryResultsSelected()
{ {
var selected = SelectedResults == Results; var selected = SelectedResults == Results;
return selected; return selected;
@ -1352,6 +1417,18 @@ namespace Flow.Launcher.ViewModel
} }
} }
/// <summary>
/// Checks if Flow Launcher should ignore any hotkeys
/// </summary>
public bool ShouldIgnoreHotkeys()
{
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
}
#endregion
#region Public Methods
public void Show() public void Show()
{ {
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
@ -1388,10 +1465,10 @@ namespace Flow.Launcher.ViewModel
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
{ {
CloseExternalPreview(); await CloseExternalPreviewAsync();
} }
if (!SelectedIsFromQueryResults()) if (!QueryResultsSelected())
{ {
SelectedResults = Results; SelectedResults = Results;
} }
@ -1401,10 +1478,7 @@ namespace Flow.Launcher.ViewModel
{ {
ChangeQueryText(string.Empty); ChangeQueryText(string.Empty);
await Task.Delay(1); // Wait for one frame to ensure UI reflects changes await Task.Delay(1); // Wait for one frame to ensure UI reflects changes
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(Application.Current.MainWindow.UpdateLayout); // Force UI update
{
Application.Current.MainWindow.UpdateLayout(); // Force UI update
});
} }
switch (Settings.LastQueryMode) switch (Settings.LastQueryMode)
@ -1463,17 +1537,8 @@ namespace Flow.Launcher.ViewModel
#pragma warning restore VSTHRD100 // Avoid async void methods #pragma warning restore VSTHRD100 // Avoid async void methods
/// <summary> /// <summary>
/// Checks if Flow Launcher should ignore any hotkeys /// Save history, user selected records and top most records
/// </summary> /// </summary>
public bool ShouldIgnoreHotkeys()
{
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
}
#endregion
#region Public Methods
public void Save() public void Save()
{ {
_historyItemsStorage.Save(); _historyItemsStorage.Save();

View file

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Drawing.Text;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
@ -6,25 +9,20 @@ using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.IO;
using System.Drawing.Text;
using System.Collections.Generic;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public class ResultViewModel : BaseModel public class ResultViewModel : BaseModel
{ {
private static PrivateFontCollection fontCollection = new(); private static readonly PrivateFontCollection FontCollection = new();
private static Dictionary<string, string> fonts = new(); private static readonly Dictionary<string, string> Fonts = new();
public ResultViewModel(Result result, Settings settings) public ResultViewModel(Result result, Settings settings)
{ {
Settings = settings; Settings = settings;
if (result == null) if (result == null) return;
{
return;
}
Result = result; Result = result;
if (Result.Glyph is { FontFamily: not null } glyph) if (Result.Glyph is { FontFamily: not null } glyph)
@ -39,20 +37,20 @@ namespace Flow.Launcher.ViewModel
fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath);
} }
if (fonts.ContainsKey(fontFamilyPath)) if (Fonts.TryGetValue(fontFamilyPath, out var value))
{ {
Glyph = glyph with Glyph = glyph with
{ {
FontFamily = fonts[fontFamilyPath] FontFamily = value
}; };
} }
else else
{ {
fontCollection.AddFontFile(fontFamilyPath); FontCollection.AddFontFile(fontFamilyPath);
fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{fontCollection.Families[^1].Name}"; Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}";
Glyph = glyph with Glyph = glyph with
{ {
FontFamily = fonts[fontFamilyPath] FontFamily = Fonts[fontFamilyPath]
}; };
} }
} }
@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel
Glyph = glyph; Glyph = glyph;
} }
} }
} }
public Settings Settings { get; } public Settings Settings { get; }
@ -95,14 +92,10 @@ namespace Flow.Launcher.ViewModel
get get
{ {
if (PreviewImageAvailable) if (PreviewImageAvailable)
{
return Visibility.Visible; return Visibility.Visible;
}
else // Fall back to icon
{ return ShowIcon;
// Fall back to icon
return ShowIcon;
}
} }
} }
@ -111,9 +104,8 @@ namespace Flow.Launcher.ViewModel
get get
{ {
if (Result.RoundedIcon) if (Result.RoundedIcon)
{
return IconXY / 2; return IconXY / 2;
}
return IconXY; return IconXY;
} }
@ -148,31 +140,40 @@ namespace Flow.Launcher.ViewModel
? Result.SubTitle ? Result.SubTitle
: Result.SubTitleToolTip; : Result.SubTitleToolTip;
private volatile bool ImageLoaded; private volatile bool _imageLoaded;
private volatile bool PreviewImageLoaded; private volatile bool _previewImageLoaded;
private ImageSource image = ImageLoader.LoadingImage; private ImageSource _image = ImageLoader.LoadingImage;
private ImageSource previewImage = ImageLoader.LoadingImage; private ImageSource _previewImage = ImageLoader.LoadingImage;
public ImageSource Image public ImageSource Image
{ {
get get
{ {
if (!ImageLoaded) if (!_imageLoaded)
{ {
ImageLoaded = true; _imageLoaded = true;
_ = LoadImageAsync(); _ = LoadImageAsync();
} }
return image; return _image;
} }
private set => image = value; private set => _image = value;
} }
public ImageSource PreviewImage public ImageSource PreviewImage
{ {
get => previewImage; get
private set => previewImage = value; {
if (!_previewImageLoaded)
{
_previewImageLoaded = true;
_ = LoadPreviewImageAsync();
}
return _previewImage;
}
private set => _previewImage = value;
} }
/// <summary> /// <summary>
@ -188,8 +189,7 @@ namespace Flow.Launcher.ViewModel
{ {
try try
{ {
var image = icon(); return icon();
return image;
} }
catch (Exception e) catch (Exception e)
{ {
@ -208,7 +208,7 @@ namespace Flow.Launcher.ViewModel
var iconDelegate = Result.Icon; var iconDelegate = Result.Icon;
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img)) if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
{ {
image = img; _image = img;
} }
else else
{ {
@ -223,7 +223,7 @@ namespace Flow.Launcher.ViewModel
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img)) if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
{ {
previewImage = img; _previewImage = img;
} }
else else
{ {
@ -234,13 +234,10 @@ namespace Flow.Launcher.ViewModel
public void LoadPreviewImage() public void LoadPreviewImage()
{ {
if (ShowDefaultPreview == Visibility.Visible) if (ShowDefaultPreview == Visibility.Visible && !_previewImageLoaded && ShowPreviewImage == Visibility.Visible)
{ {
if (!PreviewImageLoaded && ShowPreviewImage == Visibility.Visible) _previewImageLoaded = true;
{ _ = LoadPreviewImageAsync();
PreviewImageLoaded = true;
_ = LoadPreviewImageAsync();
}
} }
} }

View file

@ -1,6 +1,4 @@
using System; using System;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
@ -10,6 +8,8 @@ using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel
Results = new ResultCollection(); Results = new ResultCollection();
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock); BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
} }
public ResultsViewModel(Settings settings) : this() public ResultsViewModel(Settings settings) : this()
{ {
_settings = settings; _settings = settings;
@ -219,7 +220,6 @@ namespace Flow.Launcher.ViewModel
if (newRawResults.Count == 0) if (newRawResults.Count == 0)
return Results; return Results;
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
return Results.Where(r => r.Result.PluginID != resultId) return Results.Where(r => r.Result.PluginID != resultId)
@ -241,6 +241,7 @@ namespace Flow.Launcher.ViewModel
#endregion #endregion
#region FormattedText Dependency Property #region FormattedText Dependency Property
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText", "FormattedText",
typeof(Inline), typeof(Inline),
@ -259,8 +260,7 @@ namespace Flow.Launcher.ViewModel
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
var textBlock = d as TextBlock; if (d is not TextBlock textBlock) return;
if (textBlock == null) return;
var inline = (Inline)e.NewValue; var inline = (Inline)e.NewValue;
@ -269,6 +269,7 @@ namespace Flow.Launcher.ViewModel
textBlock.Inlines.Add(inline); textBlock.Inlines.Add(inline);
} }
#endregion #endregion
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
@ -279,7 +280,6 @@ namespace Flow.Launcher.ViewModel
public event NotifyCollectionChangedEventHandler CollectionChanged; public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
CollectionChanged?.Invoke(this, e); CollectionChanged?.Invoke(this, e);
@ -297,6 +297,7 @@ namespace Flow.Launcher.ViewModel
// wpf use DirectX / double buffered already, so just reset all won't cause ui flickering // wpf use DirectX / double buffered already, so just reset all won't cause ui flickering
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
} }
private void AddAll(List<ResultViewModel> Items) private void AddAll(List<ResultViewModel> Items)
{ {
for (int i = 0; i < Items.Count; i++) for (int i = 0; i < Items.Count; i++)
@ -308,6 +309,7 @@ namespace Flow.Launcher.ViewModel
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
} }
} }
public void RemoveAll(int Capacity = 512) public void RemoveAll(int Capacity = 512)
{ {
Clear(); Clear();