mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Fix copy to clipboard STA thread issue & Support retry for copy & Async build-in shortcut model & Fix build shortcuts text replace issue & Fix startup window hide issue (#3314)
This commit is contained in:
parent
950a7f5f2b
commit
8bb96d7f58
19 changed files with 444 additions and 160 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
searchTerms = terms;
|
||||
}
|
||||
|
||||
return new Query ()
|
||||
return new Query()
|
||||
{
|
||||
Search = search,
|
||||
RawQuery = rawQuery,
|
||||
|
|
@ -42,4 +42,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,9 +312,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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,6 +199,10 @@ namespace Flow.Launcher
|
|||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// Initialize hotkey mapper instantly after main window is created because
|
||||
// it will steal focus from main window which causes window hide
|
||||
HotKeyMapper.Initialize();
|
||||
|
||||
// Main windows needs initialized before theme change because of blur settings
|
||||
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@
|
|||
<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 -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -182,9 +181,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) =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -86,12 +86,22 @@ public partial class SettingsPanePluginsViewModel : BaseModel
|
|||
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 +112,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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ namespace Flow.Launcher.ViewModel
|
|||
private readonly UserSelectedRecord _userSelectedRecord;
|
||||
private readonly TopMostRecord _topMostRecord;
|
||||
|
||||
private CancellationTokenSource _updateSource;
|
||||
private CancellationToken _updateToken;
|
||||
private CancellationTokenSource _updateSource; // Used to cancel old query flows
|
||||
|
||||
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
|
||||
private Task _resultsViewUpdateTask;
|
||||
|
|
@ -241,7 +240,7 @@ namespace Flow.Launcher.ViewModel
|
|||
return;
|
||||
}
|
||||
|
||||
var token = e.Token == default ? _updateToken : e.Token;
|
||||
var token = e.Token == default ? _updateSource.Token : e.Token;
|
||||
|
||||
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
|
||||
var resultsCopy = DeepCloneResults(e.Results, token);
|
||||
|
|
@ -255,8 +254,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");
|
||||
}
|
||||
|
|
@ -640,7 +642,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>
|
||||
|
|
@ -648,10 +674,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;
|
||||
}
|
||||
|
||||
|
|
@ -1050,7 +1076,18 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public void Query(bool searchDelay, bool isReQuery = false)
|
||||
{
|
||||
_ = QueryAsync(searchDelay, isReQuery);
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
_ = QueryResultsAsync(searchDelay, isReQuery);
|
||||
}
|
||||
else if (ContextMenuSelected())
|
||||
{
|
||||
QueryContextMenu();
|
||||
}
|
||||
else if (HistorySelected())
|
||||
{
|
||||
QueryHistory();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task QueryAsync(bool searchDelay, bool isReQuery = false)
|
||||
|
|
@ -1160,20 +1197,45 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
_updateSource?.Cancel();
|
||||
|
||||
var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);
|
||||
var query = await ConstructQueryAsync(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);
|
||||
|
||||
var plugins = PluginManager.ValidPluginsForQuery(query);
|
||||
|
||||
if (query == null || plugins.Count == 0) // shortcut expanded
|
||||
if (query == null) // shortcut expanded
|
||||
{
|
||||
Results.Clear();
|
||||
// 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)
|
||||
|
||||
_updateSource = new CancellationTokenSource();
|
||||
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
_isQueryRunning = true;
|
||||
|
||||
// Switch to ThreadPool thread
|
||||
await TaskScheduler.Default;
|
||||
|
||||
if (_updateSource.Token.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);
|
||||
|
||||
_lastQuery = query;
|
||||
|
||||
var plugins = PluginManager.ValidPluginsForQuery(query);
|
||||
|
||||
if (plugins.Count == 1)
|
||||
{
|
||||
PluginIconPath = plugins.Single().Metadata.IcoPath;
|
||||
PluginIconSource = await App.API.LoadImageAsync(PluginIconPath);
|
||||
|
|
@ -1186,42 +1248,20 @@ namespace Flow.Launcher.ViewModel
|
|||
SearchIconVisibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
_updateSource?.Dispose();
|
||||
|
||||
var currentUpdateSource = new CancellationTokenSource();
|
||||
_updateSource = currentUpdateSource;
|
||||
_updateToken = _updateSource.Token;
|
||||
|
||||
ProgressBarVisibility = Visibility.Hidden;
|
||||
_isQueryRunning = true;
|
||||
|
||||
// Switch to ThreadPool thread
|
||||
await TaskScheduler.Default;
|
||||
|
||||
if (_updateSource.Token.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);
|
||||
|
||||
_lastQuery = query;
|
||||
|
||||
if (string.IsNullOrEmpty(query.ActionKeyword))
|
||||
// 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;
|
||||
}
|
||||
}*/
|
||||
|
||||
_ = Task.Delay(200, _updateSource.Token).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;
|
||||
}
|
||||
|
|
@ -1248,12 +1288,12 @@ namespace Flow.Launcher.ViewModel
|
|||
// nothing to do here
|
||||
}
|
||||
|
||||
if (_updateSource.Token.IsCancellationRequested)
|
||||
return;
|
||||
if (_updateSource.Token.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)
|
||||
{
|
||||
// update to hidden if this is still the current query
|
||||
|
|
@ -1269,8 +1309,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
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
|
||||
|
|
@ -1279,8 +1318,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
var results = await PluginManager.QueryForPluginAsync(plugin, query, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
IReadOnlyList<Result> resultsCopy;
|
||||
if (results == null)
|
||||
|
|
@ -1301,6 +1339,8 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query,
|
||||
token, reSelect)))
|
||||
{
|
||||
|
|
@ -1309,16 +1349,16 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
|
|
@ -1331,36 +1371,56 @@ 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(() =>
|
||||
return QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
|
||||
}
|
||||
|
||||
private async Task BuildQueryAsync(IEnumerable<BaseBuiltinShortcutModel> builtInShortcuts,
|
||||
StringBuilder queryBuilder, StringBuilder queryBuilderTmp)
|
||||
{
|
||||
var customExpanded = queryBuilder.ToString();
|
||||
|
||||
var queryChanged = false;
|
||||
|
||||
foreach (var shortcut in builtInShortcuts)
|
||||
{
|
||||
foreach (var shortcut in builtInShortcuts)
|
||||
try
|
||||
{
|
||||
try
|
||||
if (customExpanded.Contains(shortcut.Key))
|
||||
{
|
||||
if (customExpanded.Contains(shortcut.Key))
|
||||
string expansion;
|
||||
if (shortcut is BuiltinShortcutModel syncShortcut)
|
||||
{
|
||||
var expansion = shortcut.Expand();
|
||||
queryBuilder.Replace(shortcut.Key, expansion);
|
||||
queryBuilderTmp.Replace(shortcut.Key, expansion);
|
||||
expansion = syncShortcut.Expand();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
App.API.LogException(ClassName,
|
||||
$"Error when expanding shortcut {shortcut.Key}",
|
||||
e);
|
||||
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
|
||||
// use private field to avoid infinite recursion
|
||||
_queryText = queryBuilderTmp.ToString();
|
||||
|
||||
var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
|
||||
return query;
|
||||
if (queryChanged)
|
||||
{
|
||||
// show expanded builtin shortcuts
|
||||
// use private field to avoid infinite recursion
|
||||
_queryText = queryBuilderTmp.ToString();
|
||||
OnPropertyChanged(nameof(QueryText));
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOldQueryResults(Query query)
|
||||
|
|
@ -1489,6 +1549,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(() =>
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue