Skip to content

Commit e5ad777

Browse files
committed
Fix copy to clipboard STA thread issue
1 parent d7a29e5 commit e5ad777

File tree

5 files changed

+148
-15
lines changed

5 files changed

+148
-15
lines changed

Flow.Launcher.Infrastructure/NativeMethods.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ WM_KEYUP
1616
WM_SYSKEYDOWN
1717
WM_SYSKEYUP
1818

19-
EnumWindows
19+
EnumWindows
20+
21+
OleInitialize
22+
OleUninitialize

Flow.Launcher.Infrastructure/UserSettings/Settings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ public SearchPrecisionScore QuerySearchPrecision
248248
[JsonIgnore]
249249
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new()
250250
{
251-
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
251+
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", () => Win32Helper.StartSTATaskAsync(Clipboard.GetText).Result),
252252
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
253253
};
254254

Flow.Launcher.Infrastructure/Win32Helper.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using System.Runtime.InteropServices;
33
using System.Windows.Interop;
44
using System.Windows;
5+
using System.Threading.Tasks;
6+
using System.Threading;
7+
using Windows.Win32;
58

69
namespace Flow.Launcher.Infrastructure
710
{
@@ -97,5 +100,77 @@ private static void SetWindowAccent(Window w, AccentState state)
97100
Marshal.FreeHGlobal(accentPtr);
98101
}
99102
#endregion
103+
104+
#region STA Thread
105+
106+
/*
107+
Found on https://github.com/files-community/Files
108+
*/
109+
110+
public static Task StartSTATaskAsync(Action action)
111+
{
112+
var taskCompletionSource = new TaskCompletionSource();
113+
Thread thread = new(() =>
114+
{
115+
PInvoke.OleInitialize();
116+
117+
try
118+
{
119+
action();
120+
taskCompletionSource.SetResult();
121+
}
122+
catch (System.Exception)
123+
{
124+
taskCompletionSource.SetResult();
125+
}
126+
finally
127+
{
128+
PInvoke.OleUninitialize();
129+
}
130+
})
131+
{
132+
IsBackground = true,
133+
Priority = ThreadPriority.Normal
134+
};
135+
136+
thread.SetApartmentState(ApartmentState.STA);
137+
thread.Start();
138+
139+
return taskCompletionSource.Task;
140+
}
141+
142+
public static Task<T> StartSTATaskAsync<T>(Func<T> func)
143+
{
144+
var taskCompletionSource = new TaskCompletionSource<T>();
145+
146+
Thread thread = new(() =>
147+
{
148+
PInvoke.OleInitialize();
149+
150+
try
151+
{
152+
taskCompletionSource.SetResult(func());
153+
}
154+
catch (System.Exception)
155+
{
156+
taskCompletionSource.SetResult(default);
157+
}
158+
finally
159+
{
160+
PInvoke.OleUninitialize();
161+
}
162+
})
163+
{
164+
IsBackground = true,
165+
Priority = ThreadPriority.Normal
166+
};
167+
168+
thread.SetApartmentState(ApartmentState.STA);
169+
thread.Start();
170+
171+
return taskCompletionSource.Task;
172+
}
173+
174+
#endregion
100175
}
101176
}

Flow.Launcher/Languages/en.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@
336336
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
337337
<system:String x:Key="success">Success</system:String>
338338
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
339+
<system:String x:Key="failedToCopy">Failed to copy</system:String>
339340
<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>
340341

341342
<!-- Custom Query Hotkey Dialog -->

Flow.Launcher/PublicAPIInstance.cs

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -118,37 +118,91 @@ public void ShellRun(string cmd, string filename = "cmd.exe")
118118
ShellCommand.Execute(startInfo);
119119
}
120120

121-
public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true)
121+
public async void CopyToClipboard(string stringToCopy, bool directCopy = false, bool showDefaultNotification = true)
122122
{
123123
if (string.IsNullOrEmpty(stringToCopy))
124+
{
124125
return;
126+
}
125127

126128
var isFile = File.Exists(stringToCopy);
127129
if (directCopy && (isFile || Directory.Exists(stringToCopy)))
128130
{
129-
var paths = new StringCollection
131+
// Sometimes the clipboard is locked and cannot be accessed,
132+
// we need to retry a few times before giving up
133+
var exception = await RetryActionOnSTAThreadAsync(() =>
134+
{
135+
var paths = new StringCollection
130136
{
131137
stringToCopy
132138
};
133139

134-
Clipboard.SetFileDropList(paths);
135-
136-
if (showDefaultNotification)
137-
ShowMsg(
138-
$"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}",
139-
GetTranslation("completedSuccessfully"));
140+
Clipboard.SetFileDropList(paths);
141+
});
142+
143+
if (exception == null)
144+
{
145+
if (showDefaultNotification)
146+
{
147+
ShowMsg(
148+
$"{GetTranslation("copy")} {(isFile ? GetTranslation("fileTitle") : GetTranslation("folderTitle"))}",
149+
GetTranslation("completedSuccessfully"));
150+
}
151+
}
152+
else
153+
{
154+
LogException(nameof(PublicAPIInstance), "Failed to copy file/folder to clipboard", exception);
155+
ShowMsgError(GetTranslation("failedToCopy"));
156+
}
140157
}
141158
else
142159
{
143-
Clipboard.SetDataObject(stringToCopy);
160+
// Sometimes the clipboard is locked and cannot be accessed,
161+
// we need to retry a few times before giving up
162+
var exception = await RetryActionOnSTAThreadAsync(() =>
163+
{
164+
// We shouold use SetText instead of SetDataObject to avoid the clipboard being locked by other applications
165+
Clipboard.SetText(stringToCopy);
166+
});
144167

145-
if (showDefaultNotification)
146-
ShowMsg(
147-
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
148-
GetTranslation("completedSuccessfully"));
168+
if (exception == null)
169+
{
170+
if (showDefaultNotification)
171+
{
172+
ShowMsg(
173+
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
174+
GetTranslation("completedSuccessfully"));
175+
}
176+
}
177+
else
178+
{
179+
LogException(nameof(PublicAPIInstance), "Failed to copy text to clipboard", exception);
180+
ShowMsgError(GetTranslation("failedToCopy"));
181+
}
149182
}
150183
}
151184

185+
private static async Task<Exception> RetryActionOnSTAThreadAsync(Action action, int retryCount = 6, int retryDelay = 150)
186+
{
187+
for (var i = 0; i < retryCount; i++)
188+
{
189+
try
190+
{
191+
await Win32Helper.StartSTATaskAsync(action);
192+
break;
193+
}
194+
catch (Exception e)
195+
{
196+
if (i == retryCount - 1)
197+
{
198+
return e;
199+
}
200+
await Task.Delay(retryDelay);
201+
}
202+
}
203+
return null;
204+
}
205+
152206
public void StartLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Visible;
153207

154208
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;

0 commit comments

Comments
 (0)