diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 02b486a0241..6ba41e410ea 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -98,6 +98,7 @@ Português Português (Brasil) Italiano Slovenský +quicklook Tiếng Việt Droplex Preinstalled diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index e7dfb31c07a..a827e9e7ade 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Core.ExternalPlugins; +using Flow.Launcher.Core.ExternalPlugins; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -90,6 +90,48 @@ public static async Task ReloadDataAsync() }).ToArray()); } + public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true) + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static async Task CloseExternalPreviewAsync() + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.ClosePreviewAsync(), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true) + { + await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch + { + IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast), + _ => Task.CompletedTask, + }).ToArray()); + } + + public static bool UseExternalPreview() + { + return GetPluginsForInterface().Any(x => !x.Metadata.Disabled); + } + + public static bool AllowAlwaysPreview() + { + var plugin = GetPluginsForInterface().FirstOrDefault(x => !x.Metadata.Disabled); + + if (plugin is null) + return false; + + return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview(); + } + static PluginManager() { // validate user directory diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 44e10489575..0c7de10fd78 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -185,7 +185,9 @@ public CustomBrowserViewModel CustomBrowser /// when false Alphabet static service will always return empty results /// public bool ShouldUsePinyin { get; set; } = false; + public bool AlwaysPreview { get; set; } = false; + public bool AlwaysStartEn { get; set; } = false; private SearchPrecisionScore _querySearchPrecision = SearchPrecisionScore.Regular; diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs new file mode 100644 index 00000000000..cc4f94c569c --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncExternalPreview.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// This interface is for plugins that wish to provide file preview (external preview) + /// via a third party app instead of the default preview. + /// + public interface IAsyncExternalPreview : IFeatures + { + /// + /// Method for opening/showing the preview. + /// + /// The file path to open the preview for + /// Whether to send a toast message notification on failure for the user + public Task OpenPreviewAsync(string path, bool sendFailToast = true); + + /// + /// Method for closing/hiding the preview. + /// + public Task ClosePreviewAsync(); + + /// + /// Method for switching the preview to the next file result. + /// This requires the external preview be already open/showing + /// + /// The file path to switch the preview for + /// Whether to send a toast message notification on failure for the user + public Task SwitchPreviewAsync(string path, bool sendFailToast = true); + + /// + /// Allows the preview plugin to override the AlwaysPreview setting. Typically useful if plugin's preview does not + /// fully work well with being shown together when the query window appears with results. + /// When AlwaysPreview setting is on and this is set to false, the preview will not be shown when query + /// window appears with results, instead the internal preview will be shown. + /// + /// + public bool AllowAlwaysPreview(); + } +} diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index ea79386b3dd..9b42b102176 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -270,12 +270,12 @@ public record PreviewInfo /// /// Full image used for preview panel /// - public string PreviewImagePath { get; set; } + public string PreviewImagePath { get; set; } = null; /// /// Determines if the preview image should occupy the full width of the preview panel. /// - public bool IsMedia { get; set; } + public bool IsMedia { get; set; } = false; /// /// Result description text that is shown at the bottom of the preview panel. @@ -283,12 +283,17 @@ public record PreviewInfo /// /// When a value is not set, the will be used. /// - public string Description { get; set; } + public string Description { get; set; } = null; /// /// Delegate to get the preview panel's image /// - public IconDelegate PreviewDelegate { get; set; } + public IconDelegate PreviewDelegate { get; set; } = null; + + /// + /// File path of the result. For third-party programs providing external preview. + /// + public string FilePath { get; set; } = null; /// /// Default instance of @@ -299,6 +304,7 @@ public record PreviewInfo Description = null, IsMedia = false, PreviewDelegate = null, + FilePath = null, }; } } diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 3686f348d38..b312240d684 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -427,7 +427,7 @@ VerticalAlignment="Stretch" Background="Transparent" ShowsPreview="True" - Visibility="{Binding PreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}"> + Visibility="{Binding InternalPreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}"> @@ -439,7 +439,7 @@ Grid.Column="2" VerticalAlignment="Stretch" Style="{DynamicResource PreviewArea}" - Visibility="{Binding PreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}"> + Visibility="{Binding InternalPreviewVisible, Converter={StaticResource BoolToVisibilityConverter}}"> /// we need move cursor to end when we manually changed query /// but we don't want to move cursor to end when query is updated from TextBox @@ -805,10 +759,186 @@ public string VerifyOrSetDefaultHotkey(string hotkey, string defaultHotkey) public string Image => Constant.QueryTextBoxIconImagePath; public bool StartWithEnglishMode => Settings.AlwaysStartEn; + + #endregion + + #region Preview + + public bool InternalPreviewVisible + { + get + { + if (ResultAreaColumn == ResultAreaColumnPreviewShown) + return true; + + if (ResultAreaColumn == ResultAreaColumnPreviewHidden) + return false; +#if DEBUG + throw new NotImplementedException("ResultAreaColumn should match ResultAreaColumnPreviewShown/ResultAreaColumnPreviewHidden value"); +#else + Log.Error("MainViewModel", "ResultAreaColumnPreviewHidden/ResultAreaColumnPreviewShown int value not implemented", "InternalPreviewVisible"); +#endif + return false; + } + } + + private static readonly int ResultAreaColumnPreviewShown = 1; + + private static readonly int ResultAreaColumnPreviewHidden = 3; - public bool PreviewVisible { get; set; } = false; + public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown; - public int ResultAreaColumn { get; set; } = 1; + // 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 + // preview is no longer available. + public bool ExternalPreviewVisible { get; set; } = false; + + private void ShowPreview() + { + var useExternalPreview = PluginManager.UseExternalPreview(); + + switch (useExternalPreview) + { + case true + when CanExternalPreviewSelectedResult(out var path): + // Internal preview may still be on when user switches to external + if (InternalPreviewVisible) + HideInternalPreview(); + OpenExternalPreview(path); + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + CloseExternalPreview(); + ShowInternalPreview(); + break; + + case false: + ShowInternalPreview(); + break; + } + } + + private void HidePreview() + { + if (PluginManager.UseExternalPreview()) + CloseExternalPreview(); + + if (InternalPreviewVisible) + HideInternalPreview(); + } + + [RelayCommand] + private void TogglePreview() + { + if (InternalPreviewVisible || ExternalPreviewVisible) + { + HidePreview(); + } + else + { + ShowPreview(); + } + } + + private void ToggleInternalPreview() + { + if (!InternalPreviewVisible) + { + ShowInternalPreview(); + } + else + { + HideInternalPreview(); + } + } + + private void OpenExternalPreview(string path, bool sendFailToast = true) + { + _ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false); + ExternalPreviewVisible = true; + } + + private void CloseExternalPreview() + { + _ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false); + ExternalPreviewVisible = false; + } + + private void SwitchExternalPreview(string path, bool sendFailToast = true) + { + _ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false); + } + + private void ShowInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewShown; + Results.SelectedItem?.LoadPreviewImage(); + } + + private void HideInternalPreview() + { + ResultAreaColumn = ResultAreaColumnPreviewHidden; + } + + public void ResetPreview() + { + switch (Settings.AlwaysPreview) + { + case true + when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path): + OpenExternalPreview(path); + break; + + case true: + ShowInternalPreview(); + break; + + case false: + HidePreview(); + break; + } + } + + private void UpdatePreview() + { + switch (PluginManager.UseExternalPreview()) + { + case true + when CanExternalPreviewSelectedResult(out var path): + if (ExternalPreviewVisible) + { + SwitchExternalPreview(path, false); + } + else if (InternalPreviewVisible) + { + HideInternalPreview(); + OpenExternalPreview(path); + } + break; + + case true + when !CanExternalPreviewSelectedResult(out var _): + if (ExternalPreviewVisible) + { + CloseExternalPreview(); + ShowInternalPreview(); + } + break; + + case false + when InternalPreviewVisible: + Results.SelectedItem?.LoadPreviewImage(); + break; + } + } + + private bool CanExternalPreviewSelectedResult(out string path) + { + path = Results.SelectedItem?.Result?.Preview.FilePath; + return !string.IsNullOrEmpty(path); + } #endregion @@ -1232,6 +1362,9 @@ public async void Hide() lastContextMenuResult = new Result(); lastContextMenuResults = new List(); + if (ExternalPreviewVisible) + CloseExternalPreview(); + if (!SelectedIsFromQueryResults()) { SelectedResults = Results; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 71760d0aad2..02588086f68 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -102,6 +102,10 @@ internal static Result CreateFolderResult(string title, string subtitle, string AutoCompleteText = GetAutoCompleteText(title, query, path, ResultType.Folder), TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, CopyText = path, + Preview = new Result.PreviewInfo + { + FilePath = path, + }, Action = c => { if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) @@ -192,6 +196,10 @@ internal static Result CreateDriveSpaceDisplayResult(string path, string actionK Score = 500, ProgressBar = progressValue, ProgressBarColor = progressBarColor, + Preview = new Result.PreviewInfo + { + FilePath = path, + }, Action = _ => { OpenFolder(path); @@ -261,10 +269,7 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { - Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) - ? new Result.PreviewInfo { IsMedia = true, PreviewImagePath = filePath, } - : Result.PreviewInfo.Default; - + bool isMedia = IsMedia(Path.GetExtension(filePath)); var title = Path.GetFileName(filePath); @@ -275,7 +280,12 @@ internal static Result CreateFileResult(string filePath, Query query, int score Title = title, SubTitle = Path.GetDirectoryName(filePath), IcoPath = filePath, - Preview = preview, + Preview = new Result.PreviewInfo + { + IsMedia = isMedia, + PreviewImagePath = isMedia ? filePath : null, + FilePath = filePath, + }, AutoCompleteText = GetAutoCompleteText(title, query, filePath, ResultType.File), TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, Score = score,