mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into multiple_topmost
This commit is contained in:
commit
c64f3dfb30
29 changed files with 914 additions and 237 deletions
|
|
@ -25,6 +25,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
private static readonly string ClassName = nameof(PluginManager);
|
||||
|
||||
private static IEnumerable<PluginPair> _contextMenuPlugins;
|
||||
private static IEnumerable<PluginPair> _homePlugins;
|
||||
|
||||
public static List<PluginPair> AllPlugins { get; private set; }
|
||||
public static readonly HashSet<PluginPair> GlobalPlugins = new();
|
||||
|
|
@ -220,6 +221,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
API.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
|
||||
pair.Metadata.Disabled = true;
|
||||
pair.Metadata.HomeDisabled = true;
|
||||
failedPlugins.Enqueue(pair);
|
||||
}
|
||||
}));
|
||||
|
|
@ -227,6 +229,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
await Task.WhenAll(InitTasks);
|
||||
|
||||
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
|
||||
_homePlugins = GetPluginsForInterface<IAsyncHomeQuery>();
|
||||
|
||||
foreach (var plugin in AllPlugins)
|
||||
{
|
||||
// set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
|
||||
|
|
@ -274,6 +278,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
};
|
||||
}
|
||||
|
||||
public static ICollection<PluginPair> ValidPluginsForHomeQuery()
|
||||
{
|
||||
return _homePlugins.ToList();
|
||||
}
|
||||
|
||||
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
|
|
@ -318,6 +327,36 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
public static async Task<List<Result>> QueryHomeForPluginAsync(PluginPair pair, Query query, CancellationToken token)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
var metadata = pair.Metadata;
|
||||
|
||||
try
|
||||
{
|
||||
var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
|
||||
async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false));
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (results == null)
|
||||
return null;
|
||||
UpdatePluginMetadata(results, metadata, query);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
|
||||
return null;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
|
||||
{
|
||||
foreach (var r in results)
|
||||
|
|
@ -378,6 +417,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return results;
|
||||
}
|
||||
|
||||
public static bool IsHomePlugin(string id)
|
||||
{
|
||||
return _homePlugins.Any(p => p.Metadata.ID == id);
|
||||
}
|
||||
|
||||
public static bool ActionKeywordRegistered(string actionKeyword)
|
||||
{
|
||||
// this method is only checking for action keywords (defined as not '*') registration
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
|
|
@ -8,10 +8,23 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
|
||||
{
|
||||
// home query
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return new Query()
|
||||
{
|
||||
Search = string.Empty,
|
||||
RawQuery = string.Empty,
|
||||
SearchTerms = Array.Empty<string>(),
|
||||
ActionKeyword = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
// replace multiple white spaces with one white space
|
||||
var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (terms.Length == 0)
|
||||
{ // nothing was typed
|
||||
{
|
||||
// nothing was typed
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -21,19 +34,21 @@ namespace Flow.Launcher.Core.Plugin
|
|||
string[] searchTerms;
|
||||
|
||||
if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled)
|
||||
{ // use non global plugin for query
|
||||
{
|
||||
// use non global plugin for query
|
||||
actionKeyword = possibleActionKeyword;
|
||||
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
|
||||
searchTerms = terms[1..];
|
||||
}
|
||||
else
|
||||
{ // non action keyword
|
||||
{
|
||||
// non action keyword
|
||||
actionKeyword = string.Empty;
|
||||
search = rawQuery.TrimStart();
|
||||
searchTerms = terms;
|
||||
}
|
||||
|
||||
return new Query ()
|
||||
return new Query()
|
||||
{
|
||||
Search = search,
|
||||
RawQuery = rawQuery,
|
||||
|
|
@ -42,4 +57,4 @@ namespace Flow.Launcher.Core.Plugin
|
|||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
_api.LogError(ClassName, "Current theme resource not found. Initializing with default theme.");
|
||||
_oldTheme = Constant.DefaultTheme;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -126,7 +126,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
// Load a ResourceDictionary for the specified theme.
|
||||
var themeName = _settings.Theme;
|
||||
var dict = GetThemeResourceDictionary(themeName);
|
||||
|
||||
|
||||
// Apply font settings to the theme resource.
|
||||
ApplyFontSettings(dict);
|
||||
UpdateResourceDictionary(dict);
|
||||
|
|
@ -152,11 +152,11 @@ namespace Flow.Launcher.Core.Resource
|
|||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||
|
||||
|
||||
SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true);
|
||||
SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||
|
|
@ -172,7 +172,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
|
||||
|
||||
if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||
{
|
||||
|
|
@ -197,7 +197,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
// First, find the setters to remove and store them in a list
|
||||
var settersToRemove = style.Setters
|
||||
.OfType<Setter>()
|
||||
.Where(setter =>
|
||||
.Where(setter =>
|
||||
setter.Property == Control.FontFamilyProperty ||
|
||||
setter.Property == Control.FontStyleProperty ||
|
||||
setter.Property == Control.FontWeightProperty ||
|
||||
|
|
@ -227,18 +227,18 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
var settersToRemove = style.Setters
|
||||
.OfType<Setter>()
|
||||
.Where(setter =>
|
||||
.Where(setter =>
|
||||
setter.Property == TextBlock.FontFamilyProperty ||
|
||||
setter.Property == TextBlock.FontStyleProperty ||
|
||||
setter.Property == TextBlock.FontWeightProperty ||
|
||||
setter.Property == TextBlock.FontStretchProperty)
|
||||
.ToList();
|
||||
|
||||
|
||||
foreach (var setter in settersToRemove)
|
||||
{
|
||||
style.Setters.Remove(setter);
|
||||
}
|
||||
|
||||
|
||||
style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily));
|
||||
style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle));
|
||||
style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight));
|
||||
|
|
@ -421,7 +421,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
// Retrieve theme resource – always use the resource with font settings applied.
|
||||
var resourceDict = GetResourceDictionary(theme);
|
||||
|
||||
|
||||
UpdateResourceDictionary(resourceDict);
|
||||
|
||||
_settings.Theme = theme;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ MONITORINFOEXW
|
|||
WM_ENTERSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
|
||||
OleInitialize
|
||||
OleUninitialize
|
||||
|
||||
GetKeyboardLayout
|
||||
GetWindowThreadProcessId
|
||||
ActivateKeyboardLayout
|
||||
|
|
@ -53,4 +56,4 @@ INPUTLANGCHANGE_FORWARD
|
|||
LOCALE_TRANSIENT_KEYBOARD1
|
||||
LOCALE_TRANSIENT_KEYBOARD2
|
||||
LOCALE_TRANSIENT_KEYBOARD3
|
||||
LOCALE_TRANSIENT_KEYBOARD4
|
||||
LOCALE_TRANSIENT_KEYBOARD4
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
#region Base
|
||||
|
||||
public abstract class ShortcutBaseModel
|
||||
{
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Func<string> Expand { get; set; } = () => { return ""; };
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ShortcutBaseModel other &&
|
||||
|
|
@ -22,16 +22,14 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
|
||||
public class CustomShortcutModel : ShortcutBaseModel
|
||||
public class BaseCustomShortcutModel : ShortcutBaseModel
|
||||
{
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonConstructorAttribute]
|
||||
public CustomShortcutModel(string key, string value)
|
||||
public BaseCustomShortcutModel(string key, string value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
Expand = () => { return Value; };
|
||||
}
|
||||
|
||||
public void Deconstruct(out string key, out string value)
|
||||
|
|
@ -40,26 +38,69 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
value = Value;
|
||||
}
|
||||
|
||||
public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut)
|
||||
public static implicit operator (string Key, string Value)(BaseCustomShortcutModel shortcut)
|
||||
{
|
||||
return (shortcut.Key, shortcut.Value);
|
||||
}
|
||||
|
||||
public static implicit operator CustomShortcutModel((string Key, string Value) shortcut)
|
||||
public static implicit operator BaseCustomShortcutModel((string Key, string Value) shortcut)
|
||||
{
|
||||
return new CustomShortcutModel(shortcut.Key, shortcut.Value);
|
||||
return new BaseCustomShortcutModel(shortcut.Key, shortcut.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public class BuiltinShortcutModel : ShortcutBaseModel
|
||||
public class BaseBuiltinShortcutModel : ShortcutBaseModel
|
||||
{
|
||||
public string Description { get; set; }
|
||||
|
||||
public BuiltinShortcutModel(string key, string description, Func<string> expand)
|
||||
public BaseBuiltinShortcutModel(string key, string description)
|
||||
{
|
||||
Key = key;
|
||||
Description = description;
|
||||
Expand = expand ?? (() => { return ""; });
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Custom Shortcut
|
||||
|
||||
public class CustomShortcutModel : BaseCustomShortcutModel
|
||||
{
|
||||
[JsonIgnore]
|
||||
public Func<string> Expand { get; set; } = () => { return string.Empty; };
|
||||
|
||||
[JsonConstructor]
|
||||
public CustomShortcutModel(string key, string value) : base(key, value)
|
||||
{
|
||||
Expand = () => { return Value; };
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Builtin Shortcut
|
||||
|
||||
public class BuiltinShortcutModel : BaseBuiltinShortcutModel
|
||||
{
|
||||
[JsonIgnore]
|
||||
public Func<string> Expand { get; set; } = () => { return string.Empty; };
|
||||
|
||||
public BuiltinShortcutModel(string key, string description, Func<string> expand) : base(key, description)
|
||||
{
|
||||
Expand = expand ?? (() => { return string.Empty; });
|
||||
}
|
||||
}
|
||||
|
||||
public class AsyncBuiltinShortcutModel : BaseBuiltinShortcutModel
|
||||
{
|
||||
[JsonIgnore]
|
||||
public Func<Task<string>> ExpandAsync { get; set; } = () => { return Task.FromResult(string.Empty); };
|
||||
|
||||
public AsyncBuiltinShortcutModel(string key, string description, Func<Task<string>> expandAsync) : base(key, description)
|
||||
{
|
||||
ExpandAsync = expandAsync ?? (() => { return Task.FromResult(string.Empty); });
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
metadata.Disabled = settings.Disabled;
|
||||
metadata.Priority = settings.Priority;
|
||||
metadata.SearchDelayTime = settings.SearchDelayTime;
|
||||
metadata.HomeDisabled = settings.HomeDisabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -79,6 +80,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values
|
||||
ActionKeywords = metadata.ActionKeywords, // use default value
|
||||
Disabled = metadata.Disabled,
|
||||
HomeDisabled = metadata.HomeDisabled,
|
||||
Priority = metadata.Priority,
|
||||
DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values
|
||||
SearchDelayTime = metadata.SearchDelayTime, // use default value
|
||||
|
|
@ -128,5 +130,6 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
/// Used only to save the state of the plugin in settings
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
public bool HomeDisabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,24 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _showHomePage { get; set; } = true;
|
||||
public bool ShowHomePage
|
||||
{
|
||||
get => _showHomePage;
|
||||
set
|
||||
{
|
||||
if (_showHomePage != value)
|
||||
{
|
||||
_showHomePage = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowHistoryResultsForHomePage { get; set; } = false;
|
||||
public int MaxHistoryResultsToShowForHomePage { get; set; } = 5;
|
||||
|
||||
public int CustomExplorerIndex { get; set; } = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -312,9 +330,9 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
|
||||
public ObservableCollection<BaseBuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
|
||||
{
|
||||
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
|
||||
new AsyncBuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText)),
|
||||
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using System.Diagnostics;
|
|||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Markup;
|
||||
|
|
@ -337,6 +339,78 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
#endregion
|
||||
|
||||
#region STA Thread
|
||||
|
||||
/*
|
||||
Inspired by https://github.com/files-community/Files code on STA Thread handling.
|
||||
*/
|
||||
|
||||
public static Task StartSTATaskAsync(Action action)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource();
|
||||
Thread thread = new(() =>
|
||||
{
|
||||
PInvoke.OleInitialize();
|
||||
|
||||
try
|
||||
{
|
||||
action();
|
||||
taskCompletionSource.SetResult();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
taskCompletionSource.SetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PInvoke.OleUninitialize();
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.Normal
|
||||
};
|
||||
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
public static Task<T> StartSTATaskAsync<T>(Func<T> func)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
|
||||
Thread thread = new(() =>
|
||||
{
|
||||
PInvoke.OleInitialize();
|
||||
|
||||
try
|
||||
{
|
||||
taskCompletionSource.SetResult(func());
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
taskCompletionSource.SetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PInvoke.OleUninitialize();
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Priority = ThreadPriority.Normal
|
||||
};
|
||||
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyboard Layout
|
||||
|
||||
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
|
||||
|
|
|
|||
23
Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs
Normal file
23
Flow.Launcher.Plugin/Interfaces/IAsyncHomeQuery.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous Query Model for Flow Launcher When Query Text is Empty
|
||||
/// </summary>
|
||||
public interface IAsyncHomeQuery : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous Querying When Query Text is Empty
|
||||
/// </summary>
|
||||
/// <para>
|
||||
/// If the Querying method requires high IO transmission
|
||||
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncHomeQuery interface
|
||||
/// </para>
|
||||
/// <param name="token">Cancel when querying job is obsolete</param>
|
||||
/// <returns></returns>
|
||||
Task<List<Result>> HomeQueryAsync(CancellationToken token);
|
||||
}
|
||||
}
|
||||
28
Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs
Normal file
28
Flow.Launcher.Plugin/Interfaces/IHomeQuery.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronous Query Model for Flow Launcher When Query Text is Empty
|
||||
/// <para>
|
||||
/// If the Querying method requires high IO transmission
|
||||
/// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncHomeQuery interface
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IHomeQuery : IAsyncHomeQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Querying When Query Text is Empty
|
||||
/// <para>
|
||||
/// This method will be called within a Task.Run,
|
||||
/// so please avoid synchronously wait for long.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
List<Result> HomeQuery();
|
||||
|
||||
Task<List<Result>> IAsyncHomeQuery.HomeQueryAsync(CancellationToken token) => Task.Run(HomeQuery);
|
||||
}
|
||||
}
|
||||
|
|
@ -471,8 +471,11 @@ namespace Flow.Launcher.Plugin
|
|||
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the plugin manifest
|
||||
/// Get the plugin manifest.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If Flow cannot get manifest data, this could be null
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyList<UserPlugin> GetPluginManifest();
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether plugin is disabled in home query.
|
||||
/// </summary>
|
||||
public bool HomeDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin execute file path.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ namespace Flow.Launcher.Plugin
|
|||
public class Query
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw query, this includes action keyword if it has
|
||||
/// Raw query, this includes action keyword if it has.
|
||||
/// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace.
|
||||
/// We didn't recommend use this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
public string RawQuery { get; internal init; }
|
||||
|
|
@ -63,10 +64,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public string FirstSearch => SplitSearch(0);
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
private string _secondToEndSearch;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// strings from second search (including) to last search
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ using Flow.Launcher.SettingPages.ViewModels;
|
|||
using Flow.Launcher.ViewModel;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
|
|
@ -198,7 +199,11 @@ namespace Flow.Launcher
|
|||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// Main windows needs initialized before theme change because of blur settings
|
||||
// Initialize hotkey mapper instantly after main window is created because
|
||||
// it will steal focus from main window which causes window hide
|
||||
HotKeyMapper.Initialize();
|
||||
|
||||
// Initialize theme for main window
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
|
@ -239,6 +244,7 @@ namespace Flow.Launcher
|
|||
}
|
||||
}
|
||||
|
||||
[Conditional("RELEASE")]
|
||||
private void AutoUpdates()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
|
|
@ -296,13 +302,12 @@ namespace Flow.Launcher
|
|||
[Conditional("RELEASE")]
|
||||
private static void RegisterAppDomainExceptions()
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
||||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Let exception throw as normal is better for Debug
|
||||
/// </summary>
|
||||
[Conditional("RELEASE")]
|
||||
private static void RegisterTaskSchedulerUnhandledException()
|
||||
{
|
||||
TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException;
|
||||
|
|
|
|||
|
|
@ -28,18 +28,18 @@ public class DataWebRequestFactory : IWebRequestCreate
|
|||
|
||||
public DataWebResponse(Uri uri)
|
||||
{
|
||||
string uriString = uri.AbsoluteUri;
|
||||
var uriString = uri.AbsoluteUri;
|
||||
|
||||
int commaIndex = uriString.IndexOf(',');
|
||||
var headers = uriString.Substring(0, commaIndex).Split(';');
|
||||
var commaIndex = uriString.IndexOf(',');
|
||||
var headers = uriString[..commaIndex].Split(';');
|
||||
_contentType = headers[0];
|
||||
string dataString = uriString.Substring(commaIndex + 1);
|
||||
var dataString = uriString[(commaIndex + 1)..];
|
||||
_data = Convert.FromBase64String(dataString);
|
||||
}
|
||||
|
||||
public override string ContentType
|
||||
{
|
||||
get { return _contentType; }
|
||||
get => _contentType;
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
|
|
@ -48,7 +48,7 @@ public class DataWebRequestFactory : IWebRequestCreate
|
|||
|
||||
public override long ContentLength
|
||||
{
|
||||
get { return _data.Length; }
|
||||
get => _data.Length;
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
|
@ -10,33 +11,34 @@ namespace Flow.Launcher.Helper;
|
|||
|
||||
public static class ErrorReporting
|
||||
{
|
||||
private static void Report(Exception e)
|
||||
private static void Report(Exception e, [CallerMemberName] string methodName = "UnHandledException")
|
||||
{
|
||||
var logger = LogManager.GetLogger("UnHandledException");
|
||||
var logger = LogManager.GetLogger(methodName);
|
||||
logger.Fatal(ExceptionFormatter.FormatExcpetion(e));
|
||||
var reportWindow = new ReportWindow(e);
|
||||
reportWindow.Show();
|
||||
}
|
||||
|
||||
public static void UnhandledExceptionHandle(object sender, UnhandledExceptionEventArgs e)
|
||||
public static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
//handle non-ui thread exceptions
|
||||
// handle non-ui thread exceptions
|
||||
Report((Exception)e.ExceptionObject);
|
||||
}
|
||||
|
||||
public static void DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
//handle ui thread exceptions
|
||||
// handle ui thread exceptions
|
||||
Report(e.Exception);
|
||||
//prevent application exist, so the user can copy prompted error info
|
||||
// prevent application exist, so the user can copy prompted error info
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
//handle unobserved task exceptions
|
||||
// handle unobserved task exceptions on UI thread
|
||||
Application.Current.Dispatcher.Invoke(() => Report(e.Exception));
|
||||
//prevent application exit, so the user can copy the prompted error info
|
||||
// prevent application exit, so the user can copy the prompted error info
|
||||
e.SetObserved();
|
||||
}
|
||||
|
||||
public static string RuntimeInfo()
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@
|
|||
<system:String x:Key="KoreanImeOpenLinkButton">Open</system:String>
|
||||
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
|
||||
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
|
||||
<system:String x:Key="homePage">Home Page</system:String>
|
||||
<system:String x:Key="homePageToolTip">Show home page results when query text is empty.</system:String>
|
||||
<system:String x:Key="historyResultsForHomePage">Show History Results in Home Page</system:String>
|
||||
<system:String x:Key="historyResultsCountForHomePage">Maximum History Results Shown in Home Page</system:String>
|
||||
<system:String x:Key="homeToggleBoxToolTip">This can only be edited if plugin supports Home feature and Home Page is enabled.</system:String>
|
||||
|
||||
<!-- Setting Plugin -->
|
||||
<system:String x:Key="searchplugin">Search Plugin</system:String>
|
||||
|
|
@ -148,6 +153,7 @@
|
|||
<system:String x:Key="DisplayModeOnOff">Enabled</system:String>
|
||||
<system:String x:Key="DisplayModePriority">Priority</system:String>
|
||||
<system:String x:Key="DisplayModeSearchDelay">Search Delay</system:String>
|
||||
<system:String x:Key="DisplayModeHomeOnOff">Home Page</system:String>
|
||||
<system:String x:Key="currentPriority">Current Priority</system:String>
|
||||
<system:String x:Key="newPriority">New Priority</system:String>
|
||||
<system:String x:Key="priority">Priority</system:String>
|
||||
|
|
@ -394,12 +400,17 @@
|
|||
<system:String x:Key="newActionKeywordsSameAsOld">This new Action Keyword is the same as old, please choose a different one</system:String>
|
||||
<system:String x:Key="success">Success</system:String>
|
||||
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
|
||||
<system:String x:Key="failedToCopy">Failed to copy</system:String>
|
||||
<system:String x:Key="actionkeyword_tips">Enter the action keywords you like to use to start the plugin and use whitespace to divide them. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>
|
||||
|
||||
<!-- Search Delay Settings Dialog -->
|
||||
<system:String x:Key="searchDelayTimeTitle">Search Delay Time Setting</system:String>
|
||||
<system:String x:Key="searchDelayTimeTips">Input the search delay time in ms you like to use for the plugin. Input empty if you don't want to specify any, and the plugin will use default search delay time.</system:String>
|
||||
|
||||
<!-- Search Delay Settings Dialog -->
|
||||
<system:String x:Key="homeTitle">Home Page</system:String>
|
||||
<system:String x:Key="homeTips">Enable the plugin home page state if you like to show the plugin results when query is empty.</system:String>
|
||||
|
||||
<!-- Custom Query Hotkey Dialog -->
|
||||
<system:String x:Key="customeQueryHotkeyTitle">Custom Query Hotkey</system:String>
|
||||
<system:String x:Key="customeQueryHotkeyTips">Press a custom hotkey to open Flow Launcher and input the specified query automatically.</system:String>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ using System.Windows.Threading;
|
|||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Core.Resource;
|
||||
using Flow.Launcher.Helper;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.Image;
|
||||
|
|
@ -167,9 +166,6 @@ namespace Flow.Launcher
|
|||
// Force update position
|
||||
UpdatePosition();
|
||||
|
||||
// Refresh frame
|
||||
await _theme.RefreshFrameAsync();
|
||||
|
||||
// Initialize resize mode after refreshing frame
|
||||
SetupResizeMode();
|
||||
|
||||
|
|
@ -182,9 +178,6 @@ namespace Flow.Launcher
|
|||
// 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;
|
||||
|
||||
// Initialize hotkey mapper after window is loaded
|
||||
HotKeyMapper.Initialize();
|
||||
|
||||
// View model property changed event
|
||||
_viewModel.PropertyChanged += (o, e) =>
|
||||
|
|
@ -281,6 +274,12 @@ namespace Flow.Launcher
|
|||
case nameof(Settings.SettingWindowFont):
|
||||
InitializeContextMenu();
|
||||
break;
|
||||
case nameof(Settings.ShowHomePage):
|
||||
if (_viewModel.QueryResultsSelected() && string.IsNullOrEmpty(_viewModel.QueryText))
|
||||
{
|
||||
_viewModel.QueryResults();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -296,6 +295,12 @@ namespace Flow.Launcher
|
|||
DependencyPropertyDescriptor
|
||||
.FromProperty(VisibilityProperty, typeof(StackPanel))
|
||||
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
|
||||
|
||||
// Initialize query state
|
||||
if (_settings.ShowHomePage && string.IsNullOrEmpty(_viewModel.QueryText))
|
||||
{
|
||||
_viewModel.QueryResults();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnClosing(object sender, CancelEventArgs e)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ namespace Flow.Launcher
|
|||
private readonly Settings _settings;
|
||||
private readonly MainViewModel _mainVM;
|
||||
|
||||
// Must use getter to access Application.Current.Resources.MergedDictionaries so earlier
|
||||
// Must use getter to avoid accessing Application.Current.Resources.MergedDictionaries so earlier in theme constructor
|
||||
private Theme _theme;
|
||||
private Theme Theme => _theme ??= Ioc.Default.GetRequiredService<Theme>();
|
||||
|
||||
|
|
@ -69,8 +69,7 @@ namespace Flow.Launcher
|
|||
_mainVM.ChangeQueryText(query, requery);
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
|
||||
public async void RestartApp()
|
||||
{
|
||||
_mainVM.Hide();
|
||||
|
|
@ -89,8 +88,6 @@ namespace Flow.Launcher
|
|||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
|
||||
public void ShowMainWindow() => _mainVM.Show();
|
||||
|
||||
public void HideMainWindow() => _mainVM.Hide();
|
||||
|
|
@ -145,37 +142,92 @@ namespace Flow.Launcher
|
|||
ShellCommand.Execute(startInfo);
|
||||
}
|
||||
|
||||
public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true)
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
|
||||
public async void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stringToCopy))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isFile = File.Exists(stringToCopy);
|
||||
if (directCopy && (isFile || Directory.Exists(stringToCopy)))
|
||||
{
|
||||
var paths = new StringCollection
|
||||
// Sometimes the clipboard is locked and cannot be accessed,
|
||||
// we need to retry a few times before giving up
|
||||
var exception = await RetryActionOnSTAThreadAsync(() =>
|
||||
{
|
||||
var paths = new StringCollection
|
||||
{
|
||||
stringToCopy
|
||||
};
|
||||
|
||||
Clipboard.SetFileDropList(paths);
|
||||
|
||||
if (showDefaultNotification)
|
||||
ShowMsg(
|
||||
$"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}",
|
||||
GetTranslation("completedSuccessfully"));
|
||||
Clipboard.SetFileDropList(paths);
|
||||
});
|
||||
|
||||
if (exception == null)
|
||||
{
|
||||
if (showDefaultNotification)
|
||||
{
|
||||
ShowMsg(
|
||||
$"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}",
|
||||
GetTranslation("completedSuccessfully"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception);
|
||||
ShowMsgError(GetTranslation("failedToCopy"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Clipboard.SetDataObject(stringToCopy);
|
||||
// Sometimes the clipboard is locked and cannot be accessed,
|
||||
// we need to retry a few times before giving up
|
||||
var exception = await RetryActionOnSTAThreadAsync(() =>
|
||||
{
|
||||
// We should use SetText instead of SetDataObject to avoid the clipboard being locked by other applications
|
||||
Clipboard.SetText(stringToCopy);
|
||||
});
|
||||
|
||||
if (showDefaultNotification)
|
||||
ShowMsg(
|
||||
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
|
||||
GetTranslation("completedSuccessfully"));
|
||||
if (exception == null)
|
||||
{
|
||||
if (showDefaultNotification)
|
||||
{
|
||||
ShowMsg(
|
||||
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
|
||||
GetTranslation("completedSuccessfully"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception);
|
||||
ShowMsgError(GetTranslation("failedToCopy"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Exception> RetryActionOnSTAThreadAsync(Action action, int retryCount = 6, int retryDelay = 150)
|
||||
{
|
||||
for (var i = 0; i < retryCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Win32Helper.StartSTATaskAsync(action).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (i == retryCount - 1)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
await Task.Delay(retryDelay);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible;
|
||||
|
||||
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
|
||||
|
|
|
|||
|
|
@ -100,10 +100,19 @@
|
|||
ToolTipService.InitialShowDelay="0"
|
||||
ToolTipService.ShowOnDisabled="True"
|
||||
Value="{Binding PluginSearchDelayTime, Mode=TwoWay}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- Put OnOffControl after PriorityControl & SearchDelayControl so that it can display correctly -->
|
||||
<ui:ToggleSwitch
|
||||
x:Name="HomeOnOffControl"
|
||||
Margin="0 0 8 0"
|
||||
IsEnabled="{Binding HomeEnabled}"
|
||||
IsOn="{Binding PluginHomeState}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}"
|
||||
ToolTip="{DynamicResource homeToggleBoxToolTip}"
|
||||
Visibility="{Binding DataContext.IsHomeOnOffSelected, RelativeSource={RelativeSource AncestorType=ListBox}, Converter={StaticResource BooleanToVisibilityConverter}}" />
|
||||
|
||||
<ui:ToggleSwitch
|
||||
x:Name="OnOffControl"
|
||||
Margin="0 0 8 0"
|
||||
|
|
|
|||
|
|
@ -154,11 +154,22 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
|
|||
{
|
||||
Settings.SearchDelayTime = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(SearchDelayTimeDisplay));
|
||||
}
|
||||
}
|
||||
}
|
||||
public string SearchDelayTimeDisplay => $"{SearchDelayTimeValue}ms";
|
||||
|
||||
public int MaxHistoryResultsToShowValue
|
||||
{
|
||||
get => Settings.MaxHistoryResultsToShowForHomePage;
|
||||
set
|
||||
{
|
||||
if (Settings.MaxHistoryResultsToShowForHomePage != value)
|
||||
{
|
||||
Settings.MaxHistoryResultsToShowForHomePage = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEnumDropdownLocalizations()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
|
|||
}
|
||||
}
|
||||
|
||||
public IList<PluginStoreItemViewModel> ExternalPlugins =>
|
||||
App.API.GetPluginManifest()?.Select(p => new PluginStoreItemViewModel(p))
|
||||
public IList<PluginStoreItemViewModel> ExternalPlugins => App.API.GetPluginManifest()?
|
||||
.Select(p => new PluginStoreItemViewModel(p))
|
||||
.OrderByDescending(p => p.Category == PluginStoreItemViewModel.NewRelease)
|
||||
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.RecentlyUpdated)
|
||||
.ThenByDescending(p => p.Category == PluginStoreItemViewModel.None)
|
||||
|
|
|
|||
|
|
@ -80,18 +80,42 @@ public partial class SettingsPanePluginsViewModel : BaseModel
|
|||
}
|
||||
}
|
||||
|
||||
private bool _isHomeOnOffSelected;
|
||||
public bool IsHomeOnOffSelected
|
||||
{
|
||||
get => _isHomeOnOffSelected;
|
||||
set
|
||||
{
|
||||
if (_isHomeOnOffSelected != value)
|
||||
{
|
||||
_isHomeOnOffSelected = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SettingsPanePluginsViewModel(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
UpdateEnumDropdownLocalizations();
|
||||
}
|
||||
|
||||
public string FilterText { get; set; } = string.Empty;
|
||||
private string filterText = string.Empty;
|
||||
public string FilterText
|
||||
{
|
||||
get => filterText;
|
||||
set
|
||||
{
|
||||
if (filterText != value)
|
||||
{
|
||||
filterText = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PluginViewModel? SelectedPlugin { get; set; }
|
||||
|
||||
private IEnumerable<PluginViewModel>? _pluginViewModels;
|
||||
private IEnumerable<PluginViewModel> PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins
|
||||
private IList<PluginViewModel>? _pluginViewModels;
|
||||
public IList<PluginViewModel> PluginViewModels => _pluginViewModels ??= PluginManager.AllPlugins
|
||||
.OrderBy(plugin => plugin.Metadata.Disabled)
|
||||
.ThenBy(plugin => plugin.Metadata.Name)
|
||||
.Select(plugin => new PluginViewModel
|
||||
|
|
@ -102,13 +126,12 @@ public partial class SettingsPanePluginsViewModel : BaseModel
|
|||
.Where(plugin => plugin.PluginSettingsObject != null)
|
||||
.ToList();
|
||||
|
||||
public List<PluginViewModel> FilteredPluginViewModels => PluginViewModels
|
||||
.Where(v =>
|
||||
string.IsNullOrEmpty(FilterText) ||
|
||||
App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
|
||||
App.API.FuzzySearch(FilterText, v.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet()
|
||||
)
|
||||
.ToList();
|
||||
public bool SatisfiesFilter(PluginViewModel plugin)
|
||||
{
|
||||
return string.IsNullOrEmpty(FilterText) ||
|
||||
App.API.FuzzySearch(FilterText, plugin.PluginPair.Metadata.Name).IsSearchPrecisionScoreMet() ||
|
||||
App.API.FuzzySearch(FilterText, plugin.PluginPair.Metadata.Description).IsSearchPrecisionScoreMet();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenHelperAsync(Button button)
|
||||
|
|
@ -143,6 +166,18 @@ public partial class SettingsPanePluginsViewModel : BaseModel
|
|||
{
|
||||
Text = (string)Application.Current.Resources["searchDelayTimeTips"],
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = (string)Application.Current.Resources["homeTitle"],
|
||||
FontSize = 18,
|
||||
Margin = new Thickness(0, 24, 0, 10),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = (string)Application.Current.Resources["homeTips"],
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -167,16 +202,25 @@ public partial class SettingsPanePluginsViewModel : BaseModel
|
|||
IsOnOffSelected = false;
|
||||
IsPrioritySelected = true;
|
||||
IsSearchDelaySelected = false;
|
||||
IsHomeOnOffSelected = false;
|
||||
break;
|
||||
case DisplayMode.SearchDelay:
|
||||
IsOnOffSelected = false;
|
||||
IsPrioritySelected = false;
|
||||
IsSearchDelaySelected = true;
|
||||
IsHomeOnOffSelected = false;
|
||||
break;
|
||||
case DisplayMode.HomeOnOff:
|
||||
IsOnOffSelected = false;
|
||||
IsPrioritySelected = false;
|
||||
IsSearchDelaySelected = false;
|
||||
IsHomeOnOffSelected = true;
|
||||
break;
|
||||
default:
|
||||
IsOnOffSelected = true;
|
||||
IsPrioritySelected = false;
|
||||
IsSearchDelaySelected = false;
|
||||
IsHomeOnOffSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -186,5 +230,6 @@ public enum DisplayMode
|
|||
{
|
||||
OnOff,
|
||||
Priority,
|
||||
SearchDelay
|
||||
SearchDelay,
|
||||
HomeOnOff
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,17 +217,46 @@
|
|||
Title="{DynamicResource searchDelayTime}"
|
||||
Sub="{DynamicResource searchDelayTimeToolTip}"
|
||||
Type="InsideFit">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:NumberBox
|
||||
Width="120"
|
||||
Margin="0 0 0 0"
|
||||
Maximum="1000"
|
||||
Minimum="0"
|
||||
SmallChange="10"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
ValidationMode="InvalidInputOverwritten"
|
||||
Value="{Binding SearchDelayTimeValue}" />
|
||||
</StackPanel>
|
||||
<ui:NumberBox
|
||||
Width="120"
|
||||
Margin="0 0 0 0"
|
||||
Maximum="1000"
|
||||
Minimum="0"
|
||||
SmallChange="10"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
ValidationMode="InvalidInputOverwritten"
|
||||
Value="{Binding SearchDelayTimeValue}" />
|
||||
</cc:Card>
|
||||
</cc:ExCard>
|
||||
|
||||
<cc:Card
|
||||
Title="{DynamicResource homePage}"
|
||||
Margin="0 14 0 0"
|
||||
Icon=""
|
||||
Sub="{DynamicResource homePageToolTip}">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.ShowHomePage}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
|
||||
<cc:ExCard Title="{DynamicResource historyResultsForHomePage}" Icon="">
|
||||
<cc:ExCard.SideContent>
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.ShowHistoryResultsForHomePage}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:ExCard.SideContent>
|
||||
<cc:Card Title="{DynamicResource historyResultsCountForHomePage}" Type="InsideFit">
|
||||
<ui:NumberBox
|
||||
Width="120"
|
||||
Margin="0 0 0 0"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
SmallChange="5"
|
||||
SpinButtonPlacementMode="Compact"
|
||||
ValidationMode="InvalidInputOverwritten"
|
||||
Value="{Binding MaxHistoryResultsToShowValue}" />
|
||||
</cc:Card>
|
||||
</cc:ExCard>
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
FocusManager.FocusedElement="{Binding ElementName=PluginFilterTextbox}"
|
||||
KeyDown="SettingsPanePlugins_OnKeyDown"
|
||||
mc:Ignorable="d">
|
||||
<ui:Page.Resources>
|
||||
<CollectionViewSource
|
||||
x:Key="PluginCollectionView"
|
||||
Filter="PluginCollectionView_OnFilter"
|
||||
Source="{Binding PluginViewModels}" />
|
||||
</ui:Page.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="73" />
|
||||
|
|
@ -115,10 +121,9 @@
|
|||
Background="{DynamicResource Color01B}"
|
||||
FontSize="14"
|
||||
ItemContainerStyle="{StaticResource PluginList}"
|
||||
ItemsSource="{Binding FilteredPluginViewModels}"
|
||||
ItemsSource="{Binding Source={StaticResource PluginCollectionView}}"
|
||||
ScrollViewer.CanContentScroll="False"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectedItem="{Binding SelectedPlugin}"
|
||||
SnapsToDevicePixels="True"
|
||||
Style="{DynamicResource PluginListStyle}"
|
||||
VirtualizingPanel.ScrollUnit="Pixel"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Windows.Input;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Navigation;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.SettingPages.ViewModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.SettingPages.Views;
|
||||
|
||||
|
|
@ -17,12 +20,38 @@ public partial class SettingsPanePlugins
|
|||
DataContext = _viewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(SettingsPanePluginsViewModel.FilterText))
|
||||
{
|
||||
((CollectionViewSource)FindResource("PluginCollectionView")).View.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
|
||||
{
|
||||
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
base.OnNavigatingFrom(e);
|
||||
}
|
||||
|
||||
private void SettingsPanePlugins_OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key is not Key.F || Keyboard.Modifiers is not ModifierKeys.Control) return;
|
||||
PluginFilterTextbox.Focus();
|
||||
}
|
||||
|
||||
private void PluginCollectionView_OnFilter(object sender, FilterEventArgs e)
|
||||
{
|
||||
if (e.Item is not PluginViewModel plugin)
|
||||
{
|
||||
e.Accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
e.Accepted = _viewModel.SatisfiesFilter(plugin);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
private bool _isQueryRunning;
|
||||
private Query _lastQuery;
|
||||
private bool _lastIsHomeQuery;
|
||||
private string _queryTextBeforeLeaveResults;
|
||||
private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results
|
||||
|
||||
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
|
||||
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
|
||||
|
|
@ -42,14 +44,20 @@ namespace Flow.Launcher.ViewModel
|
|||
private int lastHistoryIndex = 1;
|
||||
private readonly UserSelectedRecord _userSelectedRecord;
|
||||
|
||||
private CancellationTokenSource _updateSource;
|
||||
private CancellationToken _updateToken;
|
||||
private CancellationTokenSource _updateSource; // Used to cancel old query flows
|
||||
private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token
|
||||
|
||||
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
|
||||
private Task _resultsViewUpdateTask;
|
||||
|
||||
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
|
||||
|
||||
private readonly PluginMetadata _historyMetadata = new()
|
||||
{
|
||||
ID = "298303A65D128A845D28A7B83B3968C2", // ID is for identifying the update plugin in UpdateActionAsync
|
||||
Priority = 0 // Priority is for calculating scores in UpdateResultView
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
|
@ -59,6 +67,7 @@ namespace Flow.Launcher.ViewModel
|
|||
_queryTextBeforeLeaveResults = "";
|
||||
_queryText = "";
|
||||
_lastQuery = new Query();
|
||||
_ignoredQueryText = null; // null as invalid value
|
||||
|
||||
Settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
Settings.PropertyChanged += (_, args) =>
|
||||
|
|
@ -253,8 +262,11 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
|
||||
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
|
||||
token)))
|
||||
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
|
||||
token)))
|
||||
{
|
||||
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
|
||||
}
|
||||
|
|
@ -638,7 +650,31 @@ namespace Flow.Launcher.ViewModel
|
|||
/// <param name="isReQuery">Force query even when Query Text doesn't change</param>
|
||||
public void ChangeQueryText(string queryText, bool isReQuery = false)
|
||||
{
|
||||
_ = ChangeQueryTextAsync(queryText, isReQuery);
|
||||
// Must check access so that we will not block the UI thread which causes window visibility issue
|
||||
if (!Application.Current.Dispatcher.CheckAccess())
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() => ChangeQueryText(queryText, isReQuery));
|
||||
return;
|
||||
}
|
||||
|
||||
if (QueryText != queryText)
|
||||
{
|
||||
// Change query text first
|
||||
QueryText = queryText;
|
||||
// When we are changing query from codes, we should not delay the query
|
||||
Query(false, isReQuery: false);
|
||||
|
||||
// set to false so the subsequent set true triggers
|
||||
// PropertyChanged and MoveQueryTextToEnd is called
|
||||
QueryTextCursorMovedToEnd = false;
|
||||
}
|
||||
else if (isReQuery)
|
||||
{
|
||||
// When we are re-querying, we should not delay the query
|
||||
Query(false, isReQuery: true);
|
||||
}
|
||||
|
||||
QueryTextCursorMovedToEnd = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -646,10 +682,10 @@ namespace Flow.Launcher.ViewModel
|
|||
/// </summary>
|
||||
private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false)
|
||||
{
|
||||
// Must check access so that we will not block the UI thread which cause window visibility issue
|
||||
// Must check access so that we will not block the UI thread which causes window visibility issue
|
||||
if (!Application.Current.Dispatcher.CheckAccess())
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryText(queryText, isReQuery));
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryTextAsync(queryText, isReQuery));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -702,6 +738,9 @@ namespace Flow.Launcher.ViewModel
|
|||
if (isReturningFromContextMenu)
|
||||
{
|
||||
_queryText = _queryTextBeforeLeaveResults;
|
||||
// When executing OnPropertyChanged, QueryTextBox_TextChanged1 and Query will be called
|
||||
// So we need to ignore it so that we will not call Query again
|
||||
_ignoredQueryText = _queryText;
|
||||
OnPropertyChanged(nameof(QueryText));
|
||||
QueryTextCursorMovedToEnd = true;
|
||||
}
|
||||
|
|
@ -751,8 +790,6 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectedResults.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1046,9 +1083,39 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
#region Query
|
||||
|
||||
public void QueryResults()
|
||||
{
|
||||
_ = QueryResultsAsync(false);
|
||||
}
|
||||
|
||||
public void Query(bool searchDelay, bool isReQuery = false)
|
||||
{
|
||||
_ = QueryAsync(searchDelay, isReQuery);
|
||||
if (_ignoredQueryText != null)
|
||||
{
|
||||
if (_ignoredQueryText == QueryText)
|
||||
{
|
||||
_ignoredQueryText = null;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If _ignoredQueryText does not match current QueryText, we should still execute Query
|
||||
_ignoredQueryText = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
_ = QueryResultsAsync(searchDelay, isReQuery);
|
||||
}
|
||||
else if (ContextMenuSelected())
|
||||
{
|
||||
QueryContextMenu();
|
||||
}
|
||||
else if (HistorySelected())
|
||||
{
|
||||
QueryHistory();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task QueryAsync(bool searchDelay, bool isReQuery = false)
|
||||
|
|
@ -1077,9 +1144,20 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
if (selected != null) // SelectedItem returns null if selection is empty.
|
||||
{
|
||||
var results = PluginManager.GetContextMenusForPlugin(selected);
|
||||
results.Add(ContextMenuTopMost(selected));
|
||||
results.Add(ContextMenuPluginInfo(selected.PluginID));
|
||||
List<Result> results;
|
||||
if (selected.PluginID == null) // SelectedItem from history in home page.
|
||||
{
|
||||
results = new()
|
||||
{
|
||||
ContextMenuTopMost(selected)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
results = PluginManager.GetContextMenusForPlugin(selected);
|
||||
results.Add(ContextMenuTopMost(selected));
|
||||
results.Add(ContextMenuPluginInfo(selected.PluginID));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
|
|
@ -1113,31 +1191,7 @@ namespace Flow.Launcher.ViewModel
|
|||
var query = QueryText.ToLower().Trim();
|
||||
History.Clear();
|
||||
|
||||
var results = new List<Result>();
|
||||
foreach (var h in _history.Items)
|
||||
{
|
||||
var title = App.API.GetTranslation("executeQuery");
|
||||
var time = App.API.GetTranslation("lastExecuteTime");
|
||||
var result = new Result
|
||||
{
|
||||
Title = string.Format(title, h.Query),
|
||||
SubTitle = string.Format(time, h.ExecutedDateTime),
|
||||
IcoPath = "Images\\history.png",
|
||||
Preview = new Result.PreviewInfo
|
||||
{
|
||||
PreviewImagePath = Constant.HistoryIcon,
|
||||
Description = string.Format(time, h.ExecutedDateTime)
|
||||
},
|
||||
OriginQuery = new Query { RawQuery = h.Query },
|
||||
Action = _ =>
|
||||
{
|
||||
SelectedResults = Results;
|
||||
App.API.ChangeQuery(h.Query);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
results.Add(result);
|
||||
}
|
||||
var results = GetHistoryItems(_history.Items);
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
|
|
@ -1154,41 +1208,68 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private static List<Result> GetHistoryItems(IEnumerable<HistoryItem> historyItems)
|
||||
{
|
||||
var results = new List<Result>();
|
||||
foreach (var h in historyItems)
|
||||
{
|
||||
var title = App.API.GetTranslation("executeQuery");
|
||||
var time = App.API.GetTranslation("lastExecuteTime");
|
||||
var result = new Result
|
||||
{
|
||||
Title = string.Format(title, h.Query),
|
||||
SubTitle = string.Format(time, h.ExecutedDateTime),
|
||||
IcoPath = Constant.HistoryIcon,
|
||||
OriginQuery = new Query { RawQuery = h.Query },
|
||||
Action = _ =>
|
||||
{
|
||||
App.API.BackToQueryResults();
|
||||
App.API.ChangeQuery(h.Query);
|
||||
return false;
|
||||
},
|
||||
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
|
||||
};
|
||||
results.Add(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
|
||||
{
|
||||
_updateSource?.Cancel();
|
||||
|
||||
var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);
|
||||
App.API.LogDebug(ClassName, $"Start query with text: <{QueryText}>");
|
||||
|
||||
var plugins = PluginManager.ValidPluginsForQuery(query);
|
||||
var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);
|
||||
|
||||
if (query == null || plugins.Count == 0) // shortcut expanded
|
||||
if (query == null) // shortcut expanded
|
||||
{
|
||||
Results.Clear();
|
||||
App.API.LogDebug(ClassName, $"Clear query results");
|
||||
|
||||
// Hide and clear results again because running query may show and add some results
|
||||
Results.Visibility = Visibility.Collapsed;
|
||||
Results.Clear();
|
||||
|
||||
// Reset plugin icon
|
||||
PluginIconPath = null;
|
||||
PluginIconSource = null;
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
|
||||
// Hide progress bar again because running query may set this to visible
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
return;
|
||||
}
|
||||
else if (plugins.Count == 1)
|
||||
{
|
||||
PluginIconPath = plugins.Single().Metadata.IcoPath;
|
||||
PluginIconSource = await App.API.LoadImageAsync(PluginIconPath);
|
||||
SearchIconVisibility = Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginIconPath = null;
|
||||
PluginIconSource = null;
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>");
|
||||
|
||||
var isHomeQuery = query.RawQuery == string.Empty;
|
||||
|
||||
_updateSource?.Dispose();
|
||||
|
||||
var currentUpdateSource = new CancellationTokenSource();
|
||||
_updateSource = currentUpdateSource;
|
||||
_updateToken = _updateSource.Token;
|
||||
var currentCancellationToken = _updateSource.Token;
|
||||
_updateToken = currentCancellationToken;
|
||||
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
_isQueryRunning = true;
|
||||
|
|
@ -1196,45 +1277,96 @@ namespace Flow.Launcher.ViewModel
|
|||
// Switch to ThreadPool thread
|
||||
await TaskScheduler.Default;
|
||||
|
||||
if (_updateSource.Token.IsCancellationRequested)
|
||||
return;
|
||||
if (currentCancellationToken.IsCancellationRequested) return;
|
||||
|
||||
// Update the query's IsReQuery property to true if this is a re-query
|
||||
query.IsReQuery = isReQuery;
|
||||
|
||||
// handle the exclusiveness of plugin using action keyword
|
||||
RemoveOldQueryResults(query);
|
||||
RemoveOldQueryResults(query, isHomeQuery);
|
||||
|
||||
_lastQuery = query;
|
||||
_lastIsHomeQuery = isHomeQuery;
|
||||
|
||||
if (string.IsNullOrEmpty(query.ActionKeyword))
|
||||
ICollection<PluginPair> plugins = Array.Empty<PluginPair>();
|
||||
if (isHomeQuery)
|
||||
{
|
||||
if (Settings.ShowHomePage)
|
||||
{
|
||||
plugins = PluginManager.ValidPluginsForHomeQuery();
|
||||
}
|
||||
|
||||
PluginIconPath = null;
|
||||
PluginIconSource = null;
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
plugins = PluginManager.ValidPluginsForQuery(query);
|
||||
|
||||
if (plugins.Count == 1)
|
||||
{
|
||||
PluginIconPath = plugins.Single().Metadata.IcoPath;
|
||||
PluginIconSource = await App.API.LoadImageAsync(PluginIconPath);
|
||||
SearchIconVisibility = Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginIconPath = null;
|
||||
PluginIconSource = null;
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
var validPluginNames = plugins.Select(x => $"<{x.Metadata.Name}>");
|
||||
App.API.LogDebug(ClassName, $"Valid <{plugins.Count}> plugins: {string.Join(" ", validPluginNames)}");
|
||||
|
||||
// Do not wait for performance improvement
|
||||
/*if (string.IsNullOrEmpty(query.ActionKeyword))
|
||||
{
|
||||
// Wait 15 millisecond for query change in global query
|
||||
// if query changes, return so that it won't be calculated
|
||||
await Task.Delay(15, _updateSource.Token);
|
||||
if (_updateSource.Token.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
await Task.Delay(15, currentCancellationToken);
|
||||
if (currentCancellationToken.IsCancellationRequested) return;
|
||||
}*/
|
||||
|
||||
_ = Task.Delay(200, _updateSource.Token).ContinueWith(_ =>
|
||||
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
|
||||
{
|
||||
// start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet
|
||||
if (!_updateSource.Token.IsCancellationRequested && _isQueryRunning)
|
||||
if (_isQueryRunning)
|
||||
{
|
||||
ProgressBarVisibility = Visibility.Visible;
|
||||
}
|
||||
},
|
||||
_updateSource.Token,
|
||||
currentCancellationToken,
|
||||
TaskContinuationOptions.NotOnCanceled,
|
||||
TaskScheduler.Default);
|
||||
|
||||
// plugins are ICollection, meaning LINQ will get the Count and preallocate Array
|
||||
|
||||
var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
|
||||
Task[] tasks;
|
||||
if (isHomeQuery)
|
||||
{
|
||||
false => QueryTaskAsync(plugin, _updateSource.Token),
|
||||
true => Task.CompletedTask
|
||||
}).ToArray();
|
||||
tasks = plugins.Select(plugin => plugin.Metadata.HomeDisabled switch
|
||||
{
|
||||
false => QueryTaskAsync(plugin, currentCancellationToken),
|
||||
true => Task.CompletedTask
|
||||
}).ToArray();
|
||||
|
||||
// Query history results for home page firstly so it will be put on top of the results
|
||||
if (Settings.ShowHistoryResultsForHomePage)
|
||||
{
|
||||
QueryHistoryTask(currentCancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
|
||||
{
|
||||
false => QueryTaskAsync(plugin, currentCancellationToken),
|
||||
true => Task.CompletedTask
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -1246,13 +1378,13 @@ namespace Flow.Launcher.ViewModel
|
|||
// nothing to do here
|
||||
}
|
||||
|
||||
if (_updateSource.Token.IsCancellationRequested)
|
||||
return;
|
||||
if (currentCancellationToken.IsCancellationRequested) return;
|
||||
|
||||
// this should happen once after all queries are done so progress bar should continue
|
||||
// until the end of all querying
|
||||
_isQueryRunning = false;
|
||||
if (!_updateSource.Token.IsCancellationRequested)
|
||||
|
||||
if (!currentCancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// update to hidden if this is still the current query
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
|
|
@ -1261,24 +1393,26 @@ namespace Flow.Launcher.ViewModel
|
|||
// Local function
|
||||
async Task QueryTaskAsync(PluginPair plugin, CancellationToken token)
|
||||
{
|
||||
if (searchDelay)
|
||||
App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>");
|
||||
|
||||
if (searchDelay && !isHomeQuery) // Do not delay for home query
|
||||
{
|
||||
var searchDelayTime = plugin.Metadata.SearchDelayTime ?? Settings.SearchDelayTime;
|
||||
|
||||
await Task.Delay(searchDelayTime, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
if (token.IsCancellationRequested) return;
|
||||
}
|
||||
|
||||
// Since it is wrapped within a ThreadPool Thread, the synchronous context is null
|
||||
// Task.Yield will force it to run in ThreadPool
|
||||
await Task.Yield();
|
||||
|
||||
var results = await PluginManager.QueryForPluginAsync(plugin, query, token);
|
||||
var results = isHomeQuery ?
|
||||
await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
|
||||
await PluginManager.QueryForPluginAsync(plugin, query, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
IReadOnlyList<Result> resultsCopy;
|
||||
if (results == null)
|
||||
|
|
@ -1299,24 +1433,46 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
App.API.LogDebug(ClassName, $"Update results for plugin <{plugin.Metadata.Name}>");
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query,
|
||||
token, reSelect)))
|
||||
{
|
||||
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
|
||||
}
|
||||
}
|
||||
|
||||
void QueryHistoryTask(CancellationToken token)
|
||||
{
|
||||
// Select last history results and revert its order to make sure last history results are on top
|
||||
var historyItems = _history.Items.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
|
||||
|
||||
var results = GetHistoryItems(historyItems);
|
||||
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
App.API.LogDebug(ClassName, $"Update results for history");
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, _historyMetadata, query,
|
||||
token)))
|
||||
{
|
||||
App.API.LogError(ClassName, "Unable to add item to Result Update Queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Query ConstructQuery(string queryText, IEnumerable<CustomShortcutModel> customShortcuts,
|
||||
IEnumerable<BuiltinShortcutModel> builtInShortcuts)
|
||||
private async Task<Query> ConstructQueryAsync(string queryText, IEnumerable<CustomShortcutModel> customShortcuts,
|
||||
IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(queryText))
|
||||
{
|
||||
return null;
|
||||
return QueryBuilder.Build(string.Empty, PluginManager.NonGlobalPlugins);
|
||||
}
|
||||
|
||||
StringBuilder queryBuilder = new(queryText);
|
||||
StringBuilder queryBuilderTmp = new(queryText);
|
||||
var queryBuilder = new StringBuilder(queryText);
|
||||
var queryBuilderTmp = new StringBuilder(queryText);
|
||||
|
||||
// Sorting order is important here, the reason is for matching longest shortcut by default
|
||||
foreach (var shortcut in customShortcuts.OrderByDescending(x => x.Key.Length))
|
||||
|
|
@ -1329,42 +1485,78 @@ namespace Flow.Launcher.ViewModel
|
|||
queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand());
|
||||
}
|
||||
|
||||
string customExpanded = queryBuilder.ToString();
|
||||
// Applying builtin shortcuts
|
||||
await BuildQueryAsync(builtInShortcuts, queryBuilder, queryBuilderTmp);
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
foreach (var shortcut in builtInShortcuts)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (customExpanded.Contains(shortcut.Key))
|
||||
{
|
||||
var expansion = shortcut.Expand();
|
||||
queryBuilder.Replace(shortcut.Key, expansion);
|
||||
queryBuilderTmp.Replace(shortcut.Key, expansion);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
App.API.LogException(ClassName,
|
||||
$"Error when expanding shortcut {shortcut.Key}",
|
||||
e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// show expanded builtin shortcuts
|
||||
// use private field to avoid infinite recursion
|
||||
_queryText = queryBuilderTmp.ToString();
|
||||
|
||||
var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
|
||||
return query;
|
||||
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
|
||||
}
|
||||
|
||||
private void RemoveOldQueryResults(Query query)
|
||||
private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,
|
||||
StringBuilder queryBuilder, StringBuilder queryBuilderTmp)
|
||||
{
|
||||
if (_lastQuery?.ActionKeyword != query?.ActionKeyword)
|
||||
var customExpanded = queryBuilder.ToString();
|
||||
|
||||
var queryChanged = false;
|
||||
|
||||
foreach (var shortcut in builtInShortcuts)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (customExpanded.Contains(shortcut.Key))
|
||||
{
|
||||
string expansion;
|
||||
if (shortcut is BuiltinShortcutModel syncShortcut)
|
||||
{
|
||||
expansion = syncShortcut.Expand();
|
||||
}
|
||||
else if (shortcut is AsyncBuiltinShortcutModel asyncShortcut)
|
||||
{
|
||||
expansion = await asyncShortcut.ExpandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
queryBuilder.Replace(shortcut.Key, expansion);
|
||||
queryBuilderTmp.Replace(shortcut.Key, expansion);
|
||||
queryChanged = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
App.API.LogException(ClassName, $"Error when expanding shortcut {shortcut.Key}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Show expanded builtin shortcuts
|
||||
if (queryChanged)
|
||||
{
|
||||
// Use private field to avoid infinite recursion
|
||||
_queryText = queryBuilderTmp.ToString();
|
||||
// When executing OnPropertyChanged, QueryTextBox_TextChanged1 and Query will be called
|
||||
// So we need to ignore it so that we will not call Query again
|
||||
_ignoredQueryText = _queryText;
|
||||
OnPropertyChanged(nameof(QueryText));
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOldQueryResults(Query query, bool isHomeQuery)
|
||||
{
|
||||
// If last and current query are home query, we don't need to clear the results
|
||||
if (_lastIsHomeQuery && isHomeQuery)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// If last or current query is home query, we need to clear the results
|
||||
else if (_lastIsHomeQuery || isHomeQuery)
|
||||
{
|
||||
App.API.LogDebug(ClassName, $"Remove old results");
|
||||
Results.Clear();
|
||||
}
|
||||
// If last and current query are not home query, we need to check action keyword
|
||||
else if (_lastQuery?.ActionKeyword != query?.ActionKeyword)
|
||||
{
|
||||
App.API.LogDebug(ClassName, $"Remove old results");
|
||||
Results.Clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1385,7 +1577,8 @@ namespace Flow.Launcher.ViewModel
|
|||
App.API.ShowMsg(App.API.GetTranslation("success"));
|
||||
App.API.ReQuery();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE74B")
|
||||
};
|
||||
}
|
||||
else
|
||||
|
|
@ -1394,7 +1587,6 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
Title = App.API.GetTranslation("setAsTopMostInThisQuery"),
|
||||
IcoPath = "Images\\up.png",
|
||||
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xeac2"),
|
||||
PluginDirectory = Constant.ProgramDirectory,
|
||||
Action = _ =>
|
||||
{
|
||||
|
|
@ -1402,7 +1594,8 @@ namespace Flow.Launcher.ViewModel
|
|||
App.API.ShowMsg(App.API.GetTranslation("success"));
|
||||
App.API.ReQuery();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE74A")
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1487,6 +1680,9 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public void Show()
|
||||
{
|
||||
// When application is exiting, we should not show the main window
|
||||
if (App.Exiting) return;
|
||||
|
||||
// When application is exiting, the Application.Current will be null
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
{
|
||||
|
|
@ -1535,10 +1731,7 @@ namespace Flow.Launcher.ViewModel
|
|||
await CloseExternalPreviewAsync();
|
||||
}
|
||||
|
||||
if (!QueryResultsSelected())
|
||||
{
|
||||
SelectedResults = Results;
|
||||
}
|
||||
BackToQueryResults();
|
||||
|
||||
switch (Settings.LastQueryMode)
|
||||
{
|
||||
|
|
@ -1551,7 +1744,7 @@ namespace Flow.Launcher.ViewModel
|
|||
break;
|
||||
case LastQueryMode.ActionKeywordPreserved:
|
||||
case LastQueryMode.ActionKeywordSelected:
|
||||
var newQuery = _lastQuery.ActionKeyword;
|
||||
var newQuery = _lastQuery?.ActionKeyword;
|
||||
|
||||
if (!string.IsNullOrEmpty(newQuery))
|
||||
newQuery += " ";
|
||||
|
|
|
|||
|
|
@ -75,6 +75,16 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
public bool PluginHomeState
|
||||
{
|
||||
get => !PluginPair.Metadata.HomeDisabled;
|
||||
set
|
||||
{
|
||||
PluginPair.Metadata.HomeDisabled = !value;
|
||||
PluginSettingsObject.HomeDisabled = !value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
|
|
@ -154,6 +164,7 @@ namespace Flow.Launcher.ViewModel
|
|||
public Infrastructure.UserSettings.Plugin PluginSettingsObject{ get; init; }
|
||||
public bool SearchDelayEnabled => Settings.SearchQueryResultsWithDelay;
|
||||
public string DefaultSearchDelay => Settings.SearchDelayTime.ToString();
|
||||
public bool HomeEnabled => Settings.ShowHomePage && PluginManager.IsHomePlugin(PluginPair.Metadata.ID);
|
||||
|
||||
public void OnActionKeywordsTextChanged()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,21 +3,33 @@ using System.Linq;
|
|||
|
||||
namespace Flow.Launcher.Plugin.PluginIndicator
|
||||
{
|
||||
public class Main : IPlugin, IPluginI18n
|
||||
public class Main : IPlugin, IPluginI18n, IHomeQuery
|
||||
{
|
||||
internal PluginInitContext Context { get; private set; }
|
||||
|
||||
public List<Result> Query(Query query)
|
||||
{
|
||||
return QueryResults(query);
|
||||
}
|
||||
|
||||
public List<Result> HomeQuery()
|
||||
{
|
||||
return QueryResults();
|
||||
}
|
||||
|
||||
private List<Result> QueryResults(Query query = null)
|
||||
{
|
||||
var nonGlobalPlugins = GetNonGlobalPlugins();
|
||||
var querySearch = query?.Search ?? string.Empty;
|
||||
|
||||
var results =
|
||||
from keyword in nonGlobalPlugins.Keys
|
||||
let plugin = nonGlobalPlugins[keyword].Metadata
|
||||
let keywordSearchResult = Context.API.FuzzySearch(query.Search, keyword)
|
||||
let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(query.Search, plugin.Name)
|
||||
let keywordSearchResult = Context.API.FuzzySearch(querySearch, keyword)
|
||||
let searchResult = keywordSearchResult.IsSearchPrecisionScoreMet() ? keywordSearchResult : Context.API.FuzzySearch(querySearch, plugin.Name)
|
||||
let score = searchResult.Score
|
||||
where (searchResult.IsSearchPrecisionScoreMet()
|
||||
|| string.IsNullOrEmpty(query.Search)) // To list all available action keywords
|
||||
|| string.IsNullOrEmpty(querySearch)) // To list all available action keywords
|
||||
&& !plugin.Disabled
|
||||
select new Result
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue