diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 87bb5045a..a54671d06 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -41,25 +41,26 @@ body: - type: input attributes: label: Flow Launcher Version - description: Go to "Settings" => "About". - value: v1.8.3 + description: Go to "Settings" => "About". If you are using a prerelease version please append the build number. + placeholder: "Example: 1.11.0" + validations: + required: true - type: input attributes: label: Windows Build Number - description: Run "ver" at CMD (command prompt). - value: 10.0.19043.1288 + description: Run "ver" at CMD (command prompt) or go to Windows Settings -> Systems -> About. + placeholder: "Example: 10.0.19043.1288" + validations: + required: true + - type: textarea id: logs attributes: label: Error Log description: > - Log file place: - - - The latest version place: `%AppData%\FlowLauncher\Logs\\.txt` - - - For portable mode: `%LocalAppData%\FlowLauncher\\UserData\Logs\\.txt` + From flow type 'open log location' and find log file with the corresponding date containing the error info. value: >
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index ba6faec9a..0df5228ba 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -64,3 +64,27 @@ 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/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs new file mode 100644 index 000000000..76695a4e3 --- /dev/null +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Flow.Launcher.Infrastructure +{ + public static class FileExplorerHelper + { + /// + /// Gets the path of the file explorer that is currently in the foreground + /// + public static string GetActiveExplorerPath() + { + var explorerWindow = GetActiveExplorer(); + string locationUrl = explorerWindow?.LocationURL; + return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null; + } + + /// + /// Gets the file explorer that is currently in the foreground + /// + private static dynamic GetActiveExplorer() + { + Type type = Type.GetTypeFromProgID("Shell.Application"); + if (type == null) return null; + dynamic shell = Activator.CreateInstance(type); + if (shell == null) + { + return null; + } + + var explorerWindows = new List(); + var openWindows = shell.Windows(); + for (int i = 0; i < openWindows.Count; i++) + { + var window = openWindows.Item(i); + if (window == null) continue; + + // find the desired window and make sure that it is indeed a file explorer + // we don't want the Internet Explorer or the classic control panel + // ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE" + if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe") + { + explorerWindows.Add(window); + } + } + + if (explorerWindows.Count == 0) return null; + + var zOrders = GetZOrder(explorerWindows); + + return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + /// + /// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1. + /// + private static IEnumerable GetZOrder(List hWnds) + { + var z = new int[hWnds.Count]; + for (var i = 0; i < hWnds.Count; i++) z[i] = -1; + + var index = 0; + var numRemaining = hWnds.Count; + EnumWindows((wnd, _) => + { + var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32()); + if (searchIndex != -1) + { + z[searchIndex] = index; + numRemaining--; + if (numRemaining == 0) return false; + } + index++; + return true; + }, IntPtr.Zero); + + return z; + } + } +} 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/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index a184520d0..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; } @@ -192,8 +204,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); [JsonIgnore] - public ObservableCollection BuiltinShortcuts { get; set; } = new ObservableCollection() { - new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText) + public ObservableCollection BuiltinShortcuts { get; set; } = new() + { + new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText), + new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath) }; public bool DontPromptUpdateMsg { get; set; } @@ -212,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/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 55cdcff04..31b71a1dc 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -48,7 +48,7 @@ - + all 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 1f74ea8a5..a36668fc4 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..15feae6cc 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/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index b192617cb..49c603dc8 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -176,6 +176,7 @@ Are you sure you want to delete {0} plugin hotkey? Are you sure you want to delete shortcut: {0} with expansion {1}? Get text from clipboard. + Get path from active explorer. Query window shadow effect Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited. Window Width Size 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 @@ + + + +