diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index a91287cc0..0df5228ba 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -62,3 +62,29 @@ TobiasSekan Img img resx +bak +tmp +directx +mvvm +dlg +ddd +dddd +clearlogfolder +ACCENT_ENABLE_TRANSPARENTGRADIENT +ACCENT_ENABLE_BLURBEHIND +WCA_ACCENT_POLICY +HGlobal +dopusrt +firefox +msedge +svgc +ime +zindex +txb +btn +otf +searchplugin +Noresult +wpftk +mkv +flac diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 28d57501b..f3b2eed87 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -259,9 +259,16 @@ namespace Flow.Launcher.Core.Plugin await using var registeredEvent = token.Register(() => { - if (!process.HasExited) - process.Kill(); - sourceBuffer.Dispose(); + try + { + if (!process.HasExited) + process.Kill(); + sourceBuffer.Dispose(); + } + catch (Exception e) + { + Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e); + } }); try @@ -288,7 +295,7 @@ namespace Flow.Launcher.Core.Plugin } sourceBuffer.Seek(0, SeekOrigin.Begin); - + return sourceBuffer; } @@ -376,7 +383,8 @@ namespace Flow.Launcher.Core.Plugin sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ var panel = new StackPanel { - Orientation = Orientation.Vertical, VerticalAlignment = VerticalAlignment.Center, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center, Margin = settingLabelPanelMargin }; RowDefinition gridRow = new RowDefinition(); @@ -390,8 +398,10 @@ namespace Flow.Launcher.Core.Plugin }; var desc = new TextBlock() { - Text = attribute.Description, FontSize = 12, - VerticalAlignment = VerticalAlignment.Center,Margin = settingDescMargin, + Text = attribute.Description, + FontSize = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingDescMargin, TextWrapping = TextWrapping.WrapWithOverflow }; desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); @@ -405,7 +415,7 @@ namespace Flow.Launcher.Core.Plugin panel.Children.Add(name); panel.Children.Add(desc); } - + Grid.SetColumn(panel, 0); Grid.SetRow(panel, rowCount); @@ -420,20 +430,20 @@ namespace Flow.Launcher.Core.Plugin { Text = attribute.Description.Replace("\\r\\n", "\r\n"), Margin = settingTextBlockMargin, - Padding = new Thickness(0,0,0,0), + Padding = new Thickness(0, 0, 0, 0), HorizontalAlignment = System.Windows.HorizontalAlignment.Left, TextAlignment = TextAlignment.Left, TextWrapping = TextWrapping.Wrap }; - Grid.SetColumn(contentControl, 0); - Grid.SetColumnSpan(contentControl, 2); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + break; } case "input": { @@ -449,50 +459,49 @@ namespace Flow.Launcher.Core.Plugin Settings[attribute.Name] = textBox.Text; }; contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + break; } case "inputWithFileBtn": + { + var textBox = new TextBox() { - var textBox = new TextBox() - { - Margin = new Thickness(10, 0, 0, 0), - Text = Settings[attribute.Name] as string ?? string.Empty, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - var Btn = new System.Windows.Controls.Button() - { - Margin = new Thickness(10,0,0,0), - Content = "Browse" - }; - var dockPanel = new DockPanel() - { - Margin = settingControlMargin - }; - DockPanel.SetDock(Btn, Dock.Right); - dockPanel.Children.Add(Btn); - dockPanel.Children.Add(textBox); - contentControl = dockPanel; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } + Margin = new Thickness(10, 0, 0, 0), + Text = Settings[attribute.Name] as string ?? string.Empty, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + var Btn = new System.Windows.Controls.Button() + { + Margin = new Thickness(10, 0, 0, 0), Content = "Browse" + }; + var dockPanel = new DockPanel() + { + Margin = settingControlMargin + }; + DockPanel.SetDock(Btn, Dock.Right); + dockPanel.Children.Add(Btn); + dockPanel.Children.Add(textBox); + contentControl = dockPanel; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + break; + } case "textarea": { var textBox = new TextBox() @@ -511,14 +520,14 @@ namespace Flow.Launcher.Core.Plugin Settings[attribute.Name] = ((TextBox)sender).Text; }; contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + break; } case "passwordBox": { @@ -535,14 +544,14 @@ namespace Flow.Launcher.Core.Plugin Settings[attribute.Name] = ((PasswordBox)sender).Password; }; contentControl = passwordBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + break; } case "dropdown": { @@ -559,14 +568,14 @@ namespace Flow.Launcher.Core.Plugin Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; }; contentControl = comboBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + break; } case "checkbox": var checkBox = new CheckBox @@ -592,13 +601,11 @@ namespace Flow.Launcher.Core.Plugin case "hyperlink": var hyperlink = new Hyperlink { - ToolTip = attribute.Description, - NavigateUri = attribute.url + ToolTip = attribute.Description, NavigateUri = attribute.url }; var linkbtn = new System.Windows.Controls.Button { - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - Margin = settingControlMargin + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin }; linkbtn.Content = attribute.urlLabel; @@ -619,7 +626,7 @@ namespace Flow.Launcher.Core.Plugin mainPanel.Children.Add(panel); mainPanel.Children.Add(contentControl); rowCount++; - + } return settingWindow; } diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 872c4543e..96338cf6a 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -36,7 +36,7 @@ namespace Flow.Launcher.Core.Resource { _themeDirectories.Add(DirectoryPath); _themeDirectories.Add(UserDirectoryPath); - MakesureThemeDirectoriesExist(); + MakeSureThemeDirectoriesExist(); var dicts = Application.Current.Resources.MergedDictionaries; _oldResource = dicts.First(d => @@ -55,20 +55,17 @@ namespace Flow.Launcher.Core.Resource _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } - private void MakesureThemeDirectoriesExist() + private void MakeSureThemeDirectoriesExist() { - foreach (string dir in _themeDirectories) + foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir))) { - if (!Directory.Exists(dir)) + try { - try - { - Directory.CreateDirectory(dir); - } - catch (Exception e) - { - Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e); - } + Directory.CreateDirectory(dir); + } + catch (Exception e) + { + Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e); } } } @@ -82,13 +79,14 @@ namespace Flow.Launcher.Core.Resource { if (string.IsNullOrEmpty(path)) throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - - Settings.Theme = theme; - + // reload all resources even if the theme itself hasn't changed in order to pickup changes // to things like fonts - UpdateResourceDictionary(GetResourceDictionary()); + UpdateResourceDictionary(GetResourceDictionary(theme)); + + Settings.Theme = theme; + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme if (_oldTheme != theme || theme == defaultTheme) { @@ -134,9 +132,9 @@ namespace Flow.Launcher.Core.Resource _oldResource = dictionaryToUpdate; } - private ResourceDictionary CurrentThemeResourceDictionary() + private ResourceDictionary GetThemeResourceDictionary(string theme) { - var uri = GetThemePath(Settings.Theme); + var uri = GetThemePath(theme); var dict = new ResourceDictionary { Source = new Uri(uri, UriKind.Absolute) @@ -145,10 +143,12 @@ namespace Flow.Launcher.Core.Resource return dict; } - public ResourceDictionary GetResourceDictionary() + private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(Settings.Theme); + + public ResourceDictionary GetResourceDictionary(string theme) { - var dict = CurrentThemeResourceDictionary(); - + var dict = GetThemeResourceDictionary(theme); + if (dict["QueryBoxStyle"] is Style queryBoxStyle && dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { @@ -200,6 +200,11 @@ namespace Flow.Launcher.Core.Resource return dict; } + private ResourceDictionary GetCurrentResourceDictionary( ) + { + return GetResourceDictionary(Settings.Theme); + } + public List LoadAvailableThemes() { List themes = new List(); @@ -229,7 +234,7 @@ namespace Flow.Launcher.Core.Resource public void AddDropShadowEffectToCurrentTheme() { - var dict = GetResourceDictionary(); + var dict = GetCurrentResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; @@ -273,7 +278,7 @@ namespace Flow.Launcher.Core.Resource public void RemoveDropShadowEffectFromCurrentTheme() { - var dict = CurrentThemeResourceDictionary(); + var dict = GetCurrentResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter; diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 91de8298c..3f64b273e 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -33,7 +33,7 @@ namespace Flow.Launcher.Core public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) { - await UpdateLock.WaitAsync(); + await UpdateLock.WaitAsync().ConfigureAwait(false); try { if (!silentUpdate) @@ -88,9 +88,13 @@ namespace Flow.Launcher.Core UpdateManager.RestartApp(Constant.ApplicationFileName); } } - catch (Exception e) when (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) + catch (Exception e) { - Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)) + Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + else + Log.Exception($"|Updater.UpdateApp|Error Occurred", e); + if (!silentUpdate) api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), api.GetTranslation("update_flowlauncher_check_connection")); diff --git a/Flow.Launcher.Infrastructure/Helper.cs b/Flow.Launcher.Infrastructure/Helper.cs index faa4c93b5..db575de90 100644 --- a/Flow.Launcher.Infrastructure/Helper.cs +++ b/Flow.Launcher.Infrastructure/Helper.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; @@ -16,7 +18,7 @@ namespace Flow.Launcher.Infrastructure /// /// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy /// - public static T NonNull(this T obj) + public static T NonNull(this T? obj) { if (obj == null) { diff --git a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs index 5bd97714c..b92bc0207 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Windows.Input; +using System.Windows.Navigation; namespace Flow.Launcher.Infrastructure.Hotkey { @@ -11,10 +12,10 @@ namespace Flow.Launcher.Infrastructure.Hotkey public bool Shift { get; set; } public bool Win { get; set; } public bool Ctrl { get; set; } - public Key CharKey { get; set; } + public Key CharKey { get; set; } = Key.None; - Dictionary specialSymbolDictionary = new Dictionary + private static readonly Dictionary specialSymbolDictionary = new Dictionary { {Key.Space, "Space"}, {Key.Oem3, "~"} @@ -27,19 +28,19 @@ namespace Flow.Launcher.Infrastructure.Hotkey ModifierKeys modifierKeys = ModifierKeys.None; if (Alt) { - modifierKeys = ModifierKeys.Alt; + modifierKeys |= ModifierKeys.Alt; } if (Shift) { - modifierKeys = modifierKeys | ModifierKeys.Shift; + modifierKeys |= ModifierKeys.Shift; } if (Win) { - modifierKeys = modifierKeys | ModifierKeys.Windows; + modifierKeys |= ModifierKeys.Windows; } if (Ctrl) { - modifierKeys = modifierKeys | ModifierKeys.Control; + modifierKeys |= ModifierKeys.Control; } return modifierKeys; } @@ -86,7 +87,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey Ctrl = true; keys.Remove("Ctrl"); } - if (keys.Count > 0) + if (keys.Count == 1) { string charKey = keys[0]; KeyValuePair? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey); @@ -110,36 +111,75 @@ namespace Flow.Launcher.Infrastructure.Hotkey public override string ToString() { - string text = string.Empty; + List keys = new List(); if (Ctrl) { - text += "Ctrl + "; + keys.Add("Ctrl"); } if (Alt) { - text += "Alt + "; + keys.Add("Alt"); } if (Shift) { - text += "Shift + "; + keys.Add("Shift"); } if (Win) { - text += "Win + "; + keys.Add("Win"); } if (CharKey != Key.None) { - text += specialSymbolDictionary.ContainsKey(CharKey) + keys.Add(specialSymbolDictionary.ContainsKey(CharKey) ? specialSymbolDictionary[CharKey] - : CharKey.ToString(); - } - else if (!string.IsNullOrEmpty(text)) - { - text = text.Remove(text.Length - 3); + : CharKey.ToString()); } + return string.Join(" + ", keys); + } - return text; + public bool Validate() + { + switch (CharKey) + { + case Key.LeftAlt: + case Key.RightAlt: + case Key.LeftCtrl: + case Key.RightCtrl: + case Key.LeftShift: + case Key.RightShift: + case Key.LWin: + case Key.RWin: + return false; + default: + if (ModifierKeys == ModifierKeys.None) + { + return !((CharKey >= Key.A && CharKey <= Key.Z) || + (CharKey >= Key.D0 && CharKey <= Key.D9) || + (CharKey >= Key.NumPad0 && CharKey <= Key.NumPad9)); + } + else + { + return CharKey != Key.None; + } + } + } + + public override bool Equals(object obj) + { + if (obj is HotkeyModel other) + { + return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey; + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return HashCode.Combine(ModifierKeys, CharKey); } } } diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 0083ccb87..43e7ddab7 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Globalization; using System.IO; using System.Text.Json; @@ -11,62 +12,82 @@ namespace Flow.Launcher.Infrastructure.Storage /// public class JsonStorage where T : new() { - protected T _data; + protected T? Data; + // need a new directory name public const string DirectoryName = "Settings"; public const string FileSuffix = ".json"; - public string FilePath { get; set; } - public string DirectoryPath { get; set; } + + protected string FilePath { get; init; } = null!; + + private string TempFilePath => $"{FilePath}.tmp"; + + private string BackupFilePath => $"{FilePath}.bak"; + + protected string DirectoryPath { get; init; } = null!; public T Load() { + string? serialized = null; + if (File.Exists(FilePath)) { - var serialized = File.ReadAllText(FilePath); - if (!string.IsNullOrWhiteSpace(serialized)) + serialized = File.ReadAllText(FilePath); + } + + if (!string.IsNullOrEmpty(serialized)) + { + try { - Deserialize(serialized); + Data = JsonSerializer.Deserialize(serialized) ?? TryLoadBackup() ?? LoadDefault(); } - else + catch (JsonException) { - LoadDefault(); + Data = TryLoadBackup() ?? LoadDefault(); } } else { - LoadDefault(); + Data = TryLoadBackup() ?? LoadDefault(); } - return _data.NonNull(); + + return Data.NonNull(); } - private void Deserialize(string serialized) - { - try - { - _data = JsonSerializer.Deserialize(serialized); - } - catch (JsonException e) - { - LoadDefault(); - Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e); - } - - if (_data == null) - { - LoadDefault(); - } - } - - private void LoadDefault() + private T LoadDefault() { if (File.Exists(FilePath)) { BackupOriginFile(); } - _data = new T(); - Save(); + return new T(); + } + + private T? TryLoadBackup() + { + if (!File.Exists(BackupFilePath)) + return default; + + try + { + var data = JsonSerializer.Deserialize(File.ReadAllText(BackupFilePath)); + + if (data != null) + { + Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); + File.Replace(BackupFilePath, FilePath, null); + + return data; + } + + return default; + } + catch (JsonException) + { + return default; + } } private void BackupOriginFile() @@ -82,13 +103,22 @@ namespace Flow.Launcher.Infrastructure.Storage public void Save() { - string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true }); + string serialized = JsonSerializer.Serialize(Data, + new JsonSerializerOptions + { + WriteIndented = true + }); - File.WriteAllText(FilePath, serialized); + File.WriteAllText(TempFilePath, serialized); + + if (!File.Exists(FilePath)) + { + File.Move(TempFilePath, FilePath); + } + else + { + File.Replace(TempFilePath, FilePath, BackupFilePath); + } } } - - [Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " + - "This is used only for Everything plugin v1.4.9 or below backwards compatibility")] - public class JsonStrorage : JsonStorage where T : new() { } } diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index 923a1a6b5..abe3f55b5 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -18,7 +18,7 @@ namespace Flow.Launcher.Infrastructure.Storage public PluginJsonStorage(T data) : this() { - _data = data; + Data = data; } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 753334e23..bfd7e4b87 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -13,6 +13,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public class Settings : BaseModel { private string language = "en"; + private string _theme = Constant.DefaultTheme; public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; public string OpenResultModifiers { get; set; } = KeyConstant.Alt; public string ColorScheme { get; set; } = "System"; @@ -29,7 +30,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings OnPropertyChanged(); } } - public string Theme { get; set; } = Constant.DefaultTheme; + public string Theme + { + get => _theme; + set + { + if (value == _theme) + return; + _theme = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(MaxResultsToShow)); + } + } public bool UseDropShadowEffect { get; set; } = false; public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name; public string QueryBoxFontStyle { get; set; } @@ -214,7 +226,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings } } public bool LeaveCmdOpen { get; set; } - public bool HideWhenDeactive { get; set; } = true; + public bool HideWhenDeactivated { get; set; } = true; public SearchWindowPositions SearchWindowPosition { get; set; } = SearchWindowPositions.MouseScreenCenter; public bool IgnoreHotkeysOnFullscreen { get; set; } diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index ab37dbd41..dc2923154 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -14,10 +14,10 @@ - 3.0.1 - 3.0.1 - 3.0.1 - 3.0.1 + 3.1.0 + 3.1.0 + 3.1.0 + 3.1.0 Flow.Launcher.Plugin Flow-Launcher MIT @@ -65,7 +65,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 5cb3a171a..bd8d32ff5 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -1,7 +1,9 @@ using System; using System.Diagnostics; using System.IO; +#pragma warning disable IDE0005 using System.Windows; +#pragma warning restore IDE0005 namespace Flow.Launcher.Plugin.SharedCommands { @@ -206,22 +208,16 @@ namespace Flow.Launcher.Plugin.SharedCommands /// public static string GetPreviousExistingDirectory(Func locationExists, string path) { - var previousDirectoryPath = ""; var index = path.LastIndexOf('\\'); if (index > 0 && index < (path.Length - 1)) { - previousDirectoryPath = path.Substring(0, index + 1); - if (!locationExists(previousDirectoryPath)) - { - return ""; - } + string previousDirectoryPath = path.Substring(0, index + 1); + return locationExists(previousDirectoryPath) ? previousDirectoryPath : ""; } else { return ""; } - - return previousDirectoryPath; } /// @@ -241,5 +237,33 @@ namespace Flow.Launcher.Plugin.SharedCommands return path; } + + /// + /// Returns if contains . + /// From https://stackoverflow.com/a/66877016 + /// + /// Parent path + /// Sub path + /// If , when and are equal, returns + /// + public static bool PathContains(string parentPath, string subPath, bool allowEqual = false) + { + var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath); + return (rel != "." || allowEqual) + && rel != ".." + && !rel.StartsWith("../") + && !rel.StartsWith(@"..\") + && !Path.IsPathRooted(rel); + } + + /// + /// Returns path ended with "\" + /// + /// + /// + public static string EnsureTrailingSlash(this string path) + { + return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + } } } diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs new file mode 100644 index 000000000..d16826053 --- /dev/null +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -0,0 +1,53 @@ +using Flow.Launcher.Plugin.SharedCommands; +using NUnit.Framework; + +namespace Flow.Launcher.Test +{ + [TestFixture] + + public class FilesFoldersTest + { + // Testcases from https://stackoverflow.com/a/31941905/20703207 + // Disk + [TestCase(@"c:", @"c:\foo", true)] + [TestCase(@"c:\", @"c:\foo", true)] + // Slash + [TestCase(@"c:\foo\bar\", @"c:\foo\", false)] + [TestCase(@"c:\foo\bar", @"c:\foo\", false)] + [TestCase(@"c:\foo", @"c:\foo\bar", true)] + [TestCase(@"c:\foo\", @"c:\foo\bar", true)] + // File + [TestCase(@"c:\foo", @"c:\foo\a.txt", true)] + [TestCase(@"c:\foo", @"c:/foo/a.txt", true)] + [TestCase(@"c:\FOO\a.txt", @"c:\foo", false)] + [TestCase(@"c:\foo\a.txt", @"c:\foo\", false)] + [TestCase(@"c:\foobar\a.txt", @"c:\foo", false)] + [TestCase(@"c:\foobar\a.txt", @"c:\foo\", false)] + [TestCase(@"c:\foo\", @"c:\foo.txt", false)] + // Prefix + [TestCase(@"c:\foo", @"c:\foobar", false)] + [TestCase(@"C:\Program", @"C:\Program Files\", false)] + [TestCase(@"c:\foobar", @"c:\foo\a.txt", false)] + [TestCase(@"c:\foobar\", @"c:\foo\a.txt", false)] + // Edge case + [TestCase(@"c:\foo", @"c:\foo\..\bar\baz", false)] + [TestCase(@"c:\bar", @"c:\foo\..\bar\baz", true)] + [TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)] + // Equality + [TestCase(@"c:\foo", @"c:\foo", false)] + [TestCase(@"c:\foo\", @"c:\foo", false)] + [TestCase(@"c:\foo", @"c:\foo\", false)] + public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) + { + Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); + } + + [TestCase(@"c:\foo", @"c:\foo", true)] + [TestCase(@"c:\foo\", @"c:\foo", true)] + [TestCase(@"c:\foo", @"c:\foo\", true)] + public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeTrue(string parentPath, string path, bool expectedResult) + { + Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, true)); + } + } +} diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index c0546cb41..039947a9f 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -48,13 +48,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 36f0294a9..e9d37433f 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -7,9 +7,11 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; using System; using System.Collections.Generic; +using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using static Flow.Launcher.Plugin.Explorer.Search.SearchManager; namespace Flow.Launcher.Test.Plugins { @@ -176,7 +178,7 @@ namespace Flow.Launcher.Test.Plugins var searchManager = new SearchManager(new Settings(), new PluginInitContext()); // When - var result = SearchManager.IsFileContentSearch(query.ActionKeyword); + var result = searchManager.IsFileContentSearch(query.ActionKeyword); // Then Assert.IsTrue(result, @@ -193,6 +195,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"c:\>*", true)] [TestCase(@"c:\>", true)] [TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)] + [TestCase(@"c:\SomeLocation\SomeOtherLocation", true)] public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) { // When, Given @@ -393,5 +396,68 @@ namespace Flow.Launcher.Test.Plugins // Then Assert.AreEqual(result, expectedResult); } + + [TestCase(@"c:\foo", @"c:\foo", true)] + [TestCase(@"C:\Foo\", @"c:\foo\", true)] + [TestCase(@"c:\foo", @"c:\foo\", false)] + public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(string path1, string path2, bool expectedResult) + { + // Given + var comparator = PathEqualityComparator.Instance; + var result1 = new Result + { + Title = Path.GetFileName(path1), + SubTitle = path1 + }; + var result2 = new Result + { + Title = Path.GetFileName(path2), + SubTitle = path2 + }; + + // When, Then + Assert.AreEqual(expectedResult, comparator.Equals(result1, result2)); + } + + [TestCase(@"c:\foo\", @"c:\foo\")] + [TestCase(@"C:\Foo\", @"c:\foo\")] + public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, string path2) + { + // Given + var comparator = PathEqualityComparator.Instance; + var result1 = new Result + { + Title = Path.GetFileName(path1), + SubTitle = path1 + }; + var result2 = new Result + { + Title = Path.GetFileName(path2), + SubTitle = path2 + }; + + var hash1 = comparator.GetHashCode(result1); + var hash2 = comparator.GetHashCode(result2); + + // When, Then + Assert.IsTrue(hash1 == hash2); + } + + [TestCase(@"%appdata%", true)] + [TestCase(@"%appdata%\123", true)] + [TestCase(@"c:\foo %appdata%\", false)] + [TestCase(@"c:\users\%USERNAME%\downloads", true)] + [TestCase(@"c:\downloads", false)] + [TestCase(@"%", false)] + [TestCase(@"%%", false)] + [TestCase(@"%bla%blabla%", false)] + public void GivenPath_WhenHavingEnvironmentVariableOrNot_ThenShouldBeExpected(string path, bool expectedResult) + { + // When + var result = EnvironmentVariables.HasEnvironmentVar(path); + + // Then + Assert.AreEqual(result, expectedResult); + } } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 43fa0eddb..1d398276d 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Windows; @@ -84,14 +85,14 @@ namespace Flow.Launcher Current.MainWindow = window; Current.MainWindow.Title = Constant.FlowLauncher; - + HotKeyMapper.Initialize(_mainVM); - // happlebao todo temp fix for instance code logic + // todo temp fix for instance code logic // load plugin before change language, because plugin language also needs be changed InternationalizationManager.Instance.Settings = _settings; InternationalizationManager.Instance.ChangeLanguage(_settings.Language); - // main windows needs initialized before theme change because of blur settigns + // main windows needs initialized before theme change because of blur settings ThemeManager.Instance.Settings = _settings; ThemeManager.Instance.ChangeTheme(_settings.Theme); @@ -130,20 +131,17 @@ namespace Flow.Launcher //[Conditional("RELEASE")] private void AutoUpdates() { - Task.Run(async () => + _ = Task.Run(async () => { if (_settings.AutoUpdates) { - // check udpate every 5 hours - var timer = new Timer(1000 * 60 * 60 * 5); - timer.Elapsed += async (s, e) => - { - await _updater.UpdateAppAsync(API); - }; - timer.Start(); - - // check updates on startup + // check update every 5 hours + var timer = new PeriodicTimer(TimeSpan.FromHours(5)); await _updater.UpdateAppAsync(API); + + while (await timer.WaitForNextTickAsync()) + // check updates on startup + await _updater.UpdateAppAsync(API); } }); } diff --git a/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs b/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs new file mode 100644 index 000000000..e81bb2507 --- /dev/null +++ b/Flow.Launcher/Converters/DiameterToCenterPointConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Flow.Launcher.Converters +{ + public class DiameterToCenterPointConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double d) + { + return new Point(d / 2, d / 2); + } + + return new Point(0, 0); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/Flow.Launcher/Converters/IconRadiusConverter.cs b/Flow.Launcher/Converters/IconRadiusConverter.cs new file mode 100644 index 000000000..51129cfb8 --- /dev/null +++ b/Flow.Launcher/Converters/IconRadiusConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Windows.Devices.PointOfService; + +namespace Flow.Launcher.Converters +{ + public class IconRadiusConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length != 2) + throw new ArgumentException("IconRadiusConverter must have 2 parameters"); + + return values[1] switch + { + true => (double)values[0] / 2, + false => (double)values[0], + _ => throw new ArgumentException("The second argument should be boolean", nameof(values)) + }; + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 4e5ecaae4..1143f7f72 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -90,7 +90,7 @@ - + all diff --git a/Flow.Launcher/HotkeyControl.xaml b/Flow.Launcher/HotkeyControl.xaml index 5a593d20a..acf4a21ec 100644 --- a/Flow.Launcher/HotkeyControl.xaml +++ b/Flow.Launcher/HotkeyControl.xaml @@ -48,6 +48,7 @@ Margin="0,0,18,0" VerticalContentAlignment="Center" input:InputMethod.IsInputMethodEnabled="False" + GotFocus="tbHotkey_GotFocus" LostFocus="tbHotkey_LostFocus" PreviewKeyDown="TbHotkey_OnPreviewKeyDown" TabIndex="100" /> diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index d746c8fd2..b71df9758 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -9,16 +9,11 @@ using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Plugin; using System.Threading; -using System.Windows.Interop; namespace Flow.Launcher { public partial class HotkeyControl : UserControl { - private Brush tbMsgForegroundColorOriginal; - - private string tbMsgTextOriginal; - public HotkeyModel CurrentHotkey { get; private set; } public bool CurrentHotkeyAvailable { get; private set; } @@ -29,8 +24,6 @@ namespace Flow.Launcher public HotkeyControl() { InitializeComponent(); - tbMsgTextOriginal = tbMsg.Text; - tbMsgForegroundColorOriginal = tbMsg.Foreground; } private CancellationTokenSource hotkeyUpdateSource; @@ -55,9 +48,7 @@ namespace Flow.Launcher specialKeyState.CtrlPressed, key); - var hotkeyString = hotkeyModel.ToString(); - - if (hotkeyString == tbHotkey.Text) + if (hotkeyModel.Equals(CurrentHotkey)) { return; } @@ -72,33 +63,32 @@ namespace Flow.Launcher public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true) { - CurrentHotkey = keyModel; - - tbHotkey.Text = CurrentHotkey.ToString(); + tbHotkey.Text = keyModel.ToString(); tbHotkey.Select(tbHotkey.Text.Length, 0); if (triggerValidate) { - CurrentHotkeyAvailable = CheckHotkeyAvailability(); - if (!CurrentHotkeyAvailable) - { - tbMsg.Foreground = new SolidColorBrush(Colors.Red); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable"); - } - else - { - tbMsg.Foreground = new SolidColorBrush(Colors.Green); - tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success"); - } - tbMsg.Visibility = Visibility.Visible; + bool hotkeyAvailable = CheckHotkeyAvailability(keyModel); + CurrentHotkeyAvailable = hotkeyAvailable; + SetMessage(hotkeyAvailable); OnHotkeyChanged(); var token = hotkeyUpdateSource.Token; await Task.Delay(500, token); if (token.IsCancellationRequested) return; - FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); - Keyboard.ClearFocus(); + + if (CurrentHotkeyAvailable) + { + CurrentHotkey = keyModel; + // To trigger LostFocus + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(this), null); + Keyboard.ClearFocus(); + } + } + else + { + CurrentHotkey = keyModel; } } @@ -107,14 +97,40 @@ namespace Flow.Launcher return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate); } - private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey); + private static bool CheckHotkeyAvailability(HotkeyModel hotkey) => hotkey.Validate() && HotKeyMapper.CheckAvailability(hotkey); public new bool IsFocused => tbHotkey.IsFocused; private void tbHotkey_LostFocus(object sender, RoutedEventArgs e) { - tbMsg.Text = tbMsgTextOriginal; + tbHotkey.Text = CurrentHotkey?.ToString() ?? ""; + tbHotkey.Select(tbHotkey.Text.Length, 0); + } + + private void tbHotkey_GotFocus(object sender, RoutedEventArgs e) + { + ResetMessage(); + } + + private void ResetMessage() + { + tbMsg.Text = InternationalizationManager.Instance.GetTranslation("flowlauncherPressHotkey"); tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B"); } + + private void SetMessage(bool hotkeyAvailable) + { + if (!hotkeyAvailable) + { + tbMsg.Foreground = new SolidColorBrush(Colors.Red); + tbMsg.Text = InternationalizationManager.Instance.GetTranslation("hotkeyUnavailable"); + } + else + { + tbMsg.Foreground = new SolidColorBrush(Colors.Green); + tbMsg.Text = InternationalizationManager.Instance.GetTranslation("success"); + } + tbMsg.Visibility = Visibility.Visible; + } } } diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 6ef8353d3..a9554da34 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -9,11 +9,11 @@ xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" xmlns:ui="http://schemas.modernwpf.com/2019" xmlns:vm="clr-namespace:Flow.Launcher.ViewModel" - d:DataContext="{d:DesignInstance Type=vm:MainViewModel}" Name="FlowMainWindow" Title="Flow Launcher" MinWidth="{Binding MainWindowWidth, Mode=OneWay}" MaxWidth="{Binding MainWindowWidth, Mode=OneWay}" + d:DataContext="{d:DesignInstance Type=vm:MainViewModel}" AllowDrop="True" AllowsTransparency="True" Background="Transparent" @@ -38,9 +38,9 @@ - - - + + + @@ -180,11 +180,11 @@ + Modifiers="Ctrl" /> + Modifiers="{Binding PreviewHotkey, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" /> @@ -207,12 +207,12 @@ @@ -273,9 +273,6 @@ - - diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index b2d842022..550648b24 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -485,7 +485,7 @@ namespace Flow.Launcher if (_settings.UseAnimation) await Task.Delay(100); - if (_settings.HideWhenDeactive) + if (_settings.HideWhenDeactivated) { _viewModel.Hide(); } diff --git a/Flow.Launcher/PriorityChangeWindow.xaml b/Flow.Launcher/PriorityChangeWindow.xaml index d6aadead9..c917eeffc 100644 --- a/Flow.Launcher/PriorityChangeWindow.xaml +++ b/Flow.Launcher/PriorityChangeWindow.xaml @@ -85,6 +85,8 @@ + + + +