diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 29d91dc8d..52d6fd736 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin private static PluginsSettings Settings; private static List _metadatas; - private static List _modifiedPlugins = new(); + private static readonly List _modifiedPlugins = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -61,10 +61,17 @@ namespace Flow.Launcher.Core.Plugin /// public static void Save() { - foreach (var plugin in AllPlugins) + foreach (var pluginPair in AllPlugins) { - var savable = plugin.Plugin as ISavable; - savable?.Save(); + var savable = pluginPair.Plugin as ISavable; + try + { + savable?.Save(); + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e); + } } API.SavePluginSettings(); @@ -81,14 +88,21 @@ namespace Flow.Launcher.Core.Plugin private static async Task DisposePluginAsync(PluginPair pluginPair) { - switch (pluginPair.Plugin) + try { - case IDisposable disposable: - disposable.Dispose(); - break; - case IAsyncDisposable asyncDisposable: - await asyncDisposable.DisposeAsync(); - break; + switch (pluginPair.Plugin) + { + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync(); + break; + } + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e); } } @@ -292,7 +306,7 @@ namespace Flow.Launcher.Core.Plugin { Title = $"{metadata.Name}: Failed to respond!", SubTitle = "Select this result for more info", - IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon, + IcoPath = Constant.ErrorIcon, PluginDirectory = metadata.PluginDirectory, ActionKeywordAssigned = query.ActionKeyword, PluginID = metadata.ID, @@ -369,8 +383,8 @@ namespace Flow.Launcher.Core.Plugin { // this method is only checking for action keywords (defined as not '*') registration // hence the actionKeyword != Query.GlobalPluginWildcardSign logic - return actionKeyword != Query.GlobalPluginWildcardSign - && NonGlobalPlugins.ContainsKey(actionKeyword); + return actionKeyword != Query.GlobalPluginWildcardSign + && NonGlobalPlugins.ContainsKey(actionKeyword); } /// diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index ffa17ab4d..df841dbbe 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -22,8 +22,8 @@ namespace Flow.Launcher.Core.Resource private const string DefaultFile = "en.xaml"; private const string Extension = ".xaml"; private readonly Settings _settings; - private readonly List _languageDirectories = new List(); - private readonly List _oldResources = new List(); + private readonly List _languageDirectories = new(); + private readonly List _oldResources = new(); private readonly string SystemLanguageCode; public Internationalization(Settings settings) @@ -144,7 +144,7 @@ namespace Flow.Launcher.Core.Resource _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } - private Language GetLanguageByLanguageCode(string languageCode) + private static Language GetLanguageByLanguageCode(string languageCode) { var lowercase = languageCode.ToLower(); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); @@ -239,7 +239,7 @@ namespace Flow.Launcher.Core.Resource return list; } - public string GetTranslation(string key) + public static string GetTranslation(string key) { var translation = Application.Current.TryFindResource(key); if (translation is string) @@ -257,8 +257,7 @@ namespace Flow.Launcher.Core.Resource { foreach (var p in PluginManager.GetPluginsForInterface()) { - var pluginI18N = p.Plugin as IPluginI18n; - if (pluginI18N == null) return; + if (p.Plugin is not IPluginI18n pluginI18N) return; try { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); @@ -272,11 +271,11 @@ namespace Flow.Launcher.Core.Resource } } - public string LanguageFile(string folder, string language) + private static string LanguageFile(string folder, string language) { if (Directory.Exists(folder)) { - string path = Path.Combine(folder, language); + var path = Path.Combine(folder, language); if (File.Exists(path)) { return path; @@ -284,7 +283,7 @@ namespace Flow.Launcher.Core.Resource else { Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>"); - string english = Path.Combine(folder, DefaultFile); + var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { return english; diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 43bb8dade..8ff10816c 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; @@ -40,6 +41,16 @@ namespace Flow.Launcher.Infrastructure.Storage FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } + // Let the old Program plugin get this constructor + [Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")] + public BinaryStorage(string filename, string directoryPath = null!) + { + DirectoryPath = directoryPath ?? DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); + } + public async ValueTask TryLoadAsync(T defaultData) { if (Data != null) return Data; @@ -82,8 +93,10 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - var serialized = MemoryPackSerializer.Serialize(Data); + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + var serialized = MemoryPackSerializer.Serialize(Data); File.WriteAllBytes(FilePath, serialized); } @@ -103,6 +116,9 @@ namespace Flow.Launcher.Infrastructure.Storage // so we need to pass it to SaveAsync public async ValueTask SaveAsync(T data) { + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + await using var stream = new FileStream(FilePath, FileMode.Create); await MemoryPackSerializer.SerializeAsync(stream, data); } diff --git a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs index 8b4062b6b..ca78b2f20 100644 --- a/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/FlowLauncherJsonStorage.cs @@ -17,11 +17,11 @@ namespace Flow.Launcher.Infrastructure.Storage public FlowLauncherJsonStorage() { - var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); - FilesFolders.ValidateDirectory(directoryPath); + DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName); + FilesFolders.ValidateDirectory(DirectoryPath); var filename = typeof(T).Name; - FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } public new void Save() diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index cdf3ae909..f283be59e 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -183,7 +183,10 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - string serialized = JsonSerializer.Serialize(Data, + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + + var serialized = JsonSerializer.Serialize(Data, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(TempFilePath, serialized); @@ -193,6 +196,9 @@ namespace Flow.Launcher.Infrastructure.Storage public async Task SaveAsync() { + // User may delete the directory, so we need to check it + FilesFolders.ValidateDirectory(DirectoryPath); + await using var tempOutput = File.OpenWrite(TempFilePath); await JsonSerializer.SerializeAsync(tempOutput, Data, new JsonSerializerOptions { WriteIndented = true }); diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index f9c548de8..54604a271 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; using System.Windows; @@ -517,5 +518,92 @@ namespace Flow.Launcher.Infrastructure } #endregion + + #region Korean IME + + public static bool IsWindows11() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 22000; + } + + public static bool IsKoreanIMEExist() + { + return GetLegacyKoreanIMERegistryValue() != null; + } + + public static bool IsLegacyKoreanIMEEnabled() + { + object value = GetLegacyKoreanIMERegistryValue(); + + if (value is int intValue) + { + return intValue == 1; + } + else if (value != null && int.TryParse(value.ToString(), out int parsedValue)) + { + return parsedValue == 1; + } + + return false; + } + + public static bool SetLegacyKoreanIMEEnabled(bool enable) + { + const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}"; + const string valueName = "NoTsf3Override5"; + + try + { + using RegistryKey key = Registry.CurrentUser.CreateSubKey(subKeyPath); + if (key != null) + { + int value = enable ? 1 : 0; + key.SetValue(valueName, value, RegistryValueKind.DWord); + return true; + } + } + catch (System.Exception) + { + // Ignored + } + + return false; + } + + public static object GetLegacyKoreanIMERegistryValue() + { + const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}"; + const string valueName = "NoTsf3Override5"; + + try + { + using RegistryKey key = Registry.CurrentUser.OpenSubKey(subKeyPath); + if (key != null) + { + return key.GetValue(valueName); + } + } + catch (System.Exception) + { + // Ignored + } + + return null; + } + + public static void OpenImeSettings() + { + try + { + Process.Start(new ProcessStartInfo("ms-settings:regionlanguage") { UseShellExecute = true }); + } + catch (System.Exception) + { + // Ignored + } + } + + #endregion } } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 1de5841a5..6c506cfc0 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -264,12 +264,12 @@ namespace Flow.Launcher.Plugin.SharedCommands var index = path.LastIndexOf('\\'); if (index > 0 && index < (path.Length - 1)) { - string previousDirectoryPath = path.Substring(0, index + 1); - return locationExists(previousDirectoryPath) ? previousDirectoryPath : ""; + string previousDirectoryPath = path[..(index + 1)]; + return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty; } else { - return ""; + return string.Empty; } } @@ -285,7 +285,7 @@ namespace Flow.Launcher.Plugin.SharedCommands // not full path, get previous level directory string var indexOfSeparator = path.LastIndexOf('\\'); - return path.Substring(0, indexOfSeparator + 1); + return path[..(indexOfSeparator + 1)]; } return path; diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 89faa105e..a9cd1a8b9 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -153,6 +153,7 @@ namespace Flow.Launcher RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); + RegisterTaskSchedulerUnhandledException(); var imageLoadertask = ImageLoader.InitializeAsync(); @@ -284,6 +285,15 @@ namespace Flow.Launcher AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } + /// + /// let exception throw as normal is better for Debug + /// + [Conditional("RELEASE")] + private static void RegisterTaskSchedulerUnhandledException() + { + TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException; + } + #endregion #region IDisposable diff --git a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs index ed94771f0..0d6f2e469 100644 --- a/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs +++ b/Flow.Launcher/Converters/QuerySuggestionBoxConverter.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; @@ -43,8 +44,16 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter // Check if Text will be larger than our QueryTextBox Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch); - // TODO: Obsolete warning? - var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black); + var dpi = VisualTreeHelper.GetDpi(queryTextBox); + var ft = new FormattedText( + queryTextBox.Text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + queryTextBox.FontSize, + Brushes.Black, + dpi.PixelsPerDip + ); var offset = queryTextBox.Padding.Right; diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs index 5b79c520d..b1ddba717 100644 --- a/Flow.Launcher/Helper/ErrorReporting.cs +++ b/Flow.Launcher/Helper/ErrorReporting.cs @@ -1,8 +1,10 @@ using System; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Threading; -using NLog; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Exception; +using NLog; namespace Flow.Launcher.Helper; @@ -30,6 +32,13 @@ public static class ErrorReporting e.Handled = true; } + public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + //handle unobserved task exceptions + Application.Current.Dispatcher.Invoke(() => Report(e.Exception)); + //prevent application exit, so the user can copy the prompted error info + } + public static string RuntimeInfo() { var info = diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 024258f1b..9a9b8e131 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -44,6 +44,7 @@ Game Mode Suspend the use of Hotkeys. Position Reset + Reset search window position Type here to search @@ -109,8 +110,22 @@ Shadow effect is not allowed while current theme has blur effect enabled Search Delay Adds a short delay while typing to reduce UI flicker and result load. Recommended if your typing speed is average. + Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled. Default Search Delay Time Wait time before showing results after typing stops. Higher values wait longer. (ms) + Information for Korean IME user + + The Korean input method used in Windows 11 may cause some issues in Flow Launcher. + If you experience any problems, you may need to enable "Use previous version of Korean IME". + Open Setting in Windows 11 and go to: + Time & Language > Language & Region > Korean > Language Options > Keyboard - Microsoft IME > Compatibility, + and enable "Use previous version of Microsoft IME". + + Open Language and Region System Settings + Opens the Korean IME setting location. Go to Korean > Language Options > Keyboard - Microsoft IME > Compatibility + Open + Use Previous Korean IME + You can change the Previous Korean IME settings directly from here Search Plugin diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 30afe67a1..bf7a45b1d 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -291,15 +291,15 @@ namespace Flow.Launcher { if (!CanClose) { + CanClose = true; _notifyIcon.Visible = false; App.API.SaveAppAllSettings(); e.Cancel = true; await ImageLoader.WaitSaveAsync(); await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); - // After plugins are all disposed, we can close the main window - CanClose = true; - // Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event + // After plugins are all disposed, we shutdown application to close app + // We use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event Application.Current.Shutdown(); } } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 95ef6c9f3..5438eac7d 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -38,20 +38,23 @@ namespace Flow.Launcher public class PublicAPIInstance : IPublicAPI, IRemovable { private readonly Settings _settings; - private readonly Internationalization _translater; private readonly MainViewModel _mainVM; + // Must use getter to access Application.Current.Resources.MergedDictionaries so earlier private Theme _theme; private Theme Theme => _theme ??= Ioc.Default.GetRequiredService(); + // Must use getter to avoid circular dependency + private Updater _updater; + private Updater Updater => _updater ??= Ioc.Default.GetRequiredService(); + private readonly object _saveSettingsLock = new(); #region Constructor - public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) + public PublicAPIInstance(Settings settings, MainViewModel mainVM) { _settings = settings; - _translater = translater; _mainVM = mainVM; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); @@ -100,8 +103,7 @@ namespace Flow.Launcher remove => _mainVM.VisibilityChanged -= value; } - // Must use Ioc.Default.GetRequiredService() to avoid circular dependency - public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService().UpdateAppAsync(false); + public void CheckForNewUpdate() => _ = Updater.UpdateAppAsync(false); public void SaveAppAllSettings() { @@ -178,7 +180,7 @@ namespace Flow.Launcher public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; - public string GetTranslation(string key) => _translater.GetTranslation(key); + public string GetTranslation(string key) => Internationalization.GetTranslation(key); public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); diff --git a/Flow.Launcher/Resources/Controls/InfoBar.xaml b/Flow.Launcher/Resources/Controls/InfoBar.xaml new file mode 100644 index 000000000..2ddcbdd0c --- /dev/null +++ b/Flow.Launcher/Resources/Controls/InfoBar.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + +