Skip to content
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

Fix copy to clipboard STA thread issue & Support retry for copy & Async build-in shortcut model #3314

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -43,6 +43,9 @@ MONITORINFOEXW
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE

OleInitialize
OleUninitialize

GetKeyboardLayout
GetWindowThreadProcessId
ActivateKeyboardLayout
Expand All @@ -53,4 +56,4 @@ INPUTLANGCHANGE_FORWARD
LOCALE_TRANSIENT_KEYBOARD1
LOCALE_TRANSIENT_KEYBOARD2
LOCALE_TRANSIENT_KEYBOARD3
LOCALE_TRANSIENT_KEYBOARD4
LOCALE_TRANSIENT_KEYBOARD4
67 changes: 54 additions & 13 deletions Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs
Original file line number Diff line number Diff line change
@@ -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 &&
Expand All @@ -22,16 +22,14 @@ public override int GetHashCode()
}
}

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)
Expand All @@ -40,26 +38,69 @@ public void Deconstruct(out string key, out string value)
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 Buildin 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
}
4 changes: 2 additions & 2 deletions Flow.Launcher.Infrastructure/UserSettings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,9 @@ public bool KeepMaxResults
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)
};

Expand Down
74 changes: 74 additions & 0 deletions Flow.Launcher.Infrastructure/Win32Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
Expand Down Expand Up @@ -333,6 +335,78 @@ internal static HWND GetWindowHandle(Window window, bool ensure = false)

#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";
Expand Down
1 change: 1 addition & 0 deletions Flow.Launcher/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,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 -->
Expand Down
80 changes: 67 additions & 13 deletions Flow.Launcher/PublicAPIInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,35 +145,89 @@ 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 (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"));
}
}
}

if (showDefaultNotification)
ShowMsg(
$"{GetTranslation("copy")} {GetTranslation("textTitle")}",
GetTranslation("completedSuccessfully"));
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;
Expand Down
Loading
Loading