Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 100 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
9f8e829
Delete CopyToClipboard function in JsonRPC api
Jack251970 Mar 6, 2025
d7a29e5
Revert "Delete CopyToClipboard function in JsonRPC api"
Jack251970 Mar 6, 2025
e5ad777
Fix copy to clipboard STA thread issue
Jack251970 Mar 6, 2025
04f6d14
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Mar 20, 2025
17ecebf
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Mar 20, 2025
b034f0d
Update Flow.Launcher.Infrastructure/Win32Helper.cs
Jack251970 Mar 20, 2025
c98c419
Update Flow.Launcher.Infrastructure/Win32Helper.cs
Jack251970 Mar 20, 2025
338c8e2
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Mar 26, 2025
ff2cb96
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Mar 27, 2025
817d247
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Mar 30, 2025
e0a651c
Add async buildin shortcut model
Jack251970 Mar 30, 2025
a49c465
Add async buildin shortcuts in mainvm
Jack251970 Mar 30, 2025
9897213
Improve log
Jack251970 Mar 30, 2025
123802b
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 4, 2025
246b1fb
Improve code comments
Jack251970 Apr 5, 2025
3b30209
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 9, 2025
c1602b0
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 12, 2025
8168d8a
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 13, 2025
3fc69ab
Fix typos
Jack251970 Apr 13, 2025
0314a9f
Add SetText back
Jack251970 Apr 13, 2025
ff9f1e1
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 14, 2025
8db8d5c
Add dispatcher back
Jack251970 Apr 14, 2025
f742460
Add code comments
Jack251970 Apr 14, 2025
ae7cea1
Fix COMException
Jack251970 Apr 14, 2025
3be057d
Revert "Fix COMException"
Jack251970 Apr 14, 2025
2f27fa6
Do not open report window for task scheduler exception
Jack251970 Apr 14, 2025
1b412da
Improve error reporting
Jack251970 Apr 15, 2025
370be2a
Use JoinableTaskFactory instead
Jack251970 Apr 15, 2025
0c592b1
Fix typos
Jack251970 Apr 15, 2025
d08deab
Code quality
Jack251970 Apr 15, 2025
e55f79e
Improve code comments & code quality
Jack251970 Apr 15, 2025
0eeaaea
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 15, 2025
bd1da94
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 16, 2025
882b7dc
Check ui thread access when calling ui thread
Jack251970 Apr 16, 2025
ba5a76a
Improve performance
Jack251970 Apr 16, 2025
632dbeb
Check ui thread access when calling ui thread
Jack251970 Apr 16, 2025
8f23870
Back to original style for fixing task issues
Jack251970 Apr 16, 2025
9dc9cde
Check token
Jack251970 Apr 16, 2025
5990820
Set update source null
Jack251970 Apr 16, 2025
72c2d7f
Wait update source cancellation before next update source
Jack251970 Apr 16, 2025
9242f8e
Valid plugin after
Jack251970 Apr 16, 2025
0e70519
Adjust ident
Jack251970 Apr 16, 2025
290750a
Dispose update slim
Jack251970 Apr 16, 2025
a780719
Improve code comments
Jack251970 Apr 16, 2025
00c496f
Revert "Set update source null"
Jack251970 Apr 16, 2025
5002449
Use gloabl token & remove useless check
Jack251970 Apr 16, 2025
396cd69
Do not dispose update source
Jack251970 Apr 16, 2025
893ec48
Remove unused check
Jack251970 Apr 16, 2025
a22306a
Use Query check instead of bool check
Jack251970 Apr 16, 2025
8e15e7f
Change variable name
Jack251970 Apr 16, 2025
67ed7a1
Do not use dispatcher
Jack251970 Apr 17, 2025
14cdf8a
Add cancel token check
Jack251970 Apr 17, 2025
be0c1ad
Switch to ThreadPool thread to keep UI responsive when waiting update…
Jack251970 Apr 17, 2025
43826cb
Add cancel token
Jack251970 Apr 17, 2025
5441fc4
Fix log message issue
Jack251970 Apr 17, 2025
20b5c47
Fix progress bar issue when cancelling query with empty
Jack251970 Apr 17, 2025
9eb7c9c
Wait last query to be canceled and then do resetting actions
Jack251970 Apr 17, 2025
b9e04ff
Improve code comments
Jack251970 Apr 17, 2025
236abc8
Add test codes
Jack251970 Apr 17, 2025
96bb07d
Use dispatcher for performance
Jack251970 Apr 17, 2025
56a3551
Fix performance issue
Jack251970 Apr 17, 2025
4ca8c60
Add test codes
Jack251970 Apr 17, 2025
ab95342
Improve test codes
Jack251970 Apr 17, 2025
186cd2c
Remove useless switch
Jack251970 Apr 17, 2025
4aa9a34
Fix test codes
Jack251970 Apr 17, 2025
e9a0868
Add running query check
Jack251970 Apr 17, 2025
854e61d
Add test codes
Jack251970 Apr 17, 2025
7961615
Improve test codes
Jack251970 Apr 17, 2025
e5b5ec5
Improve test codes
Jack251970 Apr 17, 2025
30a840a
Improve test codes
Jack251970 Apr 17, 2025
b918d00
Improve display performance
Jack251970 Apr 17, 2025
adde491
Suppress warning
Jack251970 Apr 18, 2025
6859232
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 18, 2025
c78a558
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 19, 2025
c0b792c
Set running query null after update lock and update cancellation
Jack251970 Apr 20, 2025
7669bba
Inline update source
Jack251970 Apr 22, 2025
ccfc6fd
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 22, 2025
282f486
Fix query input check issue
Jack251970 Apr 22, 2025
a143cb4
Improve code comments
Jack251970 Apr 22, 2025
1ffbbca
Remove _updateToken
Jack251970 Apr 22, 2025
836d09e
Ignore main window show when exiting
Jack251970 Apr 23, 2025
6bb638b
Fix window blink issue
Jack251970 Apr 23, 2025
dd467e2
Fix typo
Jack251970 Apr 25, 2025
023f511
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 25, 2025
74f274a
Add hotkey mapper initialization back
Jack251970 Apr 25, 2025
7800540
Improve code quality
Jack251970 Apr 25, 2025
15e6754
Fix code comments
Jack251970 Apr 25, 2025
7a879c9
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 25, 2025
fa49c08
Add ConfigureAwait(false) to slightly reduce the risk of deadlocks
Jack251970 Apr 25, 2025
ebc81c0
Improve code quality
Jack251970 Apr 25, 2025
593a360
Remove selected item binding & Use filter instead of data change
Jack251970 Apr 25, 2025
4283ef5
Enlarge update flow number
Jack251970 Apr 27, 2025
0347703
Remove update lock
Jack251970 Apr 27, 2025
ebaffc6
Fix result update issue
Jack251970 Apr 27, 2025
d446824
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 29, 2025
cb673f6
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 30, 2025
96eb673
Merge branch 'dev' into delete_clipboard_jsonrpc
Jack251970 Apr 30, 2025
68e1aad
Revert logic change
Jack251970 Apr 30, 2025
1cec01c
Merge branch 'delete_clipboard_jsonrpc' of https://github.com/Jack251…
Jack251970 Apr 30, 2025
d023ecd
Make ConstructQuery async
Jack251970 May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Flow.Launcher.Infrastructure/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP

EnumWindows
EnumWindows

OleInitialize
OleUninitialize
2 changes: 1 addition & 1 deletion Flow.Launcher.Infrastructure/UserSettings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public SearchPrecisionScore QuerySearchPrecision
[JsonIgnore]
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
{
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText).Result),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to use GetAwaiter().GetResult().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe async void is better? I am not sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either is ok?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to use GetAwaiter().GetResult().

This cannot work here, so I choose to keep as it was

Copy link
Member

@taooceros taooceros Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by not work here? It is almost semantically equal to Task.Result, except that the exception is handled in a different way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing () => Win32Helper.StartSTATaskAsync(Clipboard.GetText).Result to Win32Helper.StartSTATaskAsync(Clipboard.GetText).GetAwaiter().GetResult cannot work, the result is always empty

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will test it.

Copy link
Preview

Copilot AI Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using .Result to synchronously wait on an async STA task may lead to deadlocks or UI blocking; consider refactoring to await the result asynchronously if possible.

Suggested change
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText).Result),
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", async () => await Win32Helper.StartSTATaskAsync(Clipboard.GetText)),

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it causes build issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually how do you think about making the BuiltinShortcutModel supports Async operation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, it is a good idea

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
};

Expand Down
75 changes: 75 additions & 0 deletions Flow.Launcher.Infrastructure/Win32Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using System.Runtime.InteropServices;
using System.Windows.Interop;
using System.Windows;
using System.Threading.Tasks;
using System.Threading;
using Windows.Win32;

namespace Flow.Launcher.Infrastructure
{
Expand Down Expand Up @@ -97,5 +100,77 @@ private static void SetWindowAccent(Window w, AccentState state)
Marshal.FreeHGlobal(accentPtr);
}
#endregion

#region STA Thread

/*
Found on https://github.com/files-community/Files
*/

public static Task StartSTATaskAsync(Action action)
{
var taskCompletionSource = new TaskCompletionSource();
Thread thread = new(() =>
{
PInvoke.OleInitialize();

try
{
action();
taskCompletionSource.SetResult();
}
catch (System.Exception)
{
taskCompletionSource.SetResult();
}
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)
{
taskCompletionSource.SetResult(default);
}
finally
{
PInvoke.OleUninitialize();
}
})
{
IsBackground = true,
Priority = ThreadPriority.Normal
};

thread.SetApartmentState(ApartmentState.STA);
thread.Start();

return taskCompletionSource.Task;
}

#endregion
}
}
1 change: 1 addition & 0 deletions Flow.Launcher/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, 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 keyword you like to use to start the plugin. Use * if you don't want to specify any, and the plugin will be triggered without any action keywords.</system:String>

<!-- Custom Query Hotkey Dialog -->
Expand Down
80 changes: 67 additions & 13 deletions Flow.Launcher/PublicAPIInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,37 +118,91 @@ public void ShellRun(string cmd, string filename = "cmd.exe")
ShellCommand.Execute(startInfo);
}

public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true)
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 shouold 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);
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;
Expand Down
Loading