diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 8500e7aa444..2b91ca6b744 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -39,8 +39,10 @@ public void Save() public bool ShowOpenResultHotkey { get; set; } = true; public double WindowSize { get; set; } = 580; public string PreviewHotkey { get; set; } = $"F1"; - public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab"; + public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Alt} + Right"; public string AutoCompleteHotkey2 { get; set; } = $""; + public string DeleteWordHotkey { get; set; } = $"{KeyConstant.Alt} + Left"; + public string DeleteWordHotkey2 { get; set; } = $""; public string SelectNextItemHotkey { get; set; } = $"Tab"; public string SelectNextItemHotkey2 { get; set; } = $""; public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab"; @@ -364,6 +366,10 @@ public List RegisteredHotkeys list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = "")); if(!string.IsNullOrEmpty(AutoCompleteHotkey2)) list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = "")); + if(!string.IsNullOrEmpty(DeleteWordHotkey)) + list.Add(new(DeleteWordHotkey, "deleteWordHotkey", () => DeleteWordHotkey = "")); + if(!string.IsNullOrEmpty(DeleteWordHotkey2)) + list.Add(new(DeleteWordHotkey2, "deleteWordHotkey", () => DeleteWordHotkey2 = "")); if(!string.IsNullOrEmpty(SelectNextItemHotkey)) list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = "")); if(!string.IsNullOrEmpty(SelectNextItemHotkey2)) diff --git a/Flow.Launcher/Converters/NullToVisibilityConverter.cs b/Flow.Launcher/Converters/NullToVisibilityConverter.cs new file mode 100644 index 00000000000..11e3c4cd705 --- /dev/null +++ b/Flow.Launcher/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using System.Windows; + +namespace Flow.Launcher.Converters +{ + public class NullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => string.IsNullOrWhiteSpace(value?.ToString()) ? Visibility.Collapsed : Visibility.Visible; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index 26272712757..a06ac1b21bf 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -106,6 +106,8 @@ public enum HotkeyType SelectNextPageHotkey, AutoCompleteHotkey, AutoCompleteHotkey2, + DeleteWordHotkey, + DeleteWordHotkey2, SelectPrevItemHotkey, SelectPrevItemHotkey2, SelectNextItemHotkey, @@ -136,6 +138,8 @@ public string Hotkey HotkeyType.SelectNextPageHotkey => _settings.SelectNextPageHotkey, HotkeyType.AutoCompleteHotkey => _settings.AutoCompleteHotkey, HotkeyType.AutoCompleteHotkey2 => _settings.AutoCompleteHotkey2, + HotkeyType.DeleteWordHotkey => _settings.DeleteWordHotkey, + HotkeyType.DeleteWordHotkey2 => _settings.DeleteWordHotkey2, HotkeyType.SelectPrevItemHotkey => _settings.SelectPrevItemHotkey, HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, @@ -184,6 +188,12 @@ public string Hotkey case HotkeyType.AutoCompleteHotkey2: _settings.AutoCompleteHotkey2 = value; break; + case HotkeyType.DeleteWordHotkey: + _settings.DeleteWordHotkey = value; + break; + case HotkeyType.DeleteWordHotkey2: + _settings.DeleteWordHotkey2 = value; + break; case HotkeyType.SelectPrevItemHotkey: _settings.SelectPrevItemHotkey = value; break; diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 5a6fd3d74bf..6eeae3c6ae3 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -44,7 +44,7 @@ Game Mode Suspend the use of Hotkeys. Position Reset - Reset search window position + Reset search window location Type here to search @@ -58,7 +58,7 @@ Error setting launch on startup Hide Flow Launcher when focus is lost Do not show new version notifications - Search Window Position + Search Window Location Remember Last Position Monitor with Mouse Cursor Monitor with Focused Window @@ -259,6 +259,8 @@ Show result selection hotkey with results. Auto Complete Runs autocomplete for the selected items. + Delete Word Backwards + Deletes text by word. For paths, deletes by path segment. Select Next Item Select Previous Item Next Page @@ -488,6 +490,18 @@ Let's Start Flow Launcher Finished. Enjoy Flow Launcher. Don't forget the hotkey to start :) + Pick your style + Based on your selected style, Flow will customize the initial settings for you. This setting can be changed later. + CLI Friendly + Recommended for users familiar with the CLI or Terminal environment. + TAB is used as the key to Auto Complete. + Pressing Enter will open the path in Flow. + Windows Friendly + Recommended for users familiar with the Windows operating style. + TAB is used as the key to switch between selectable options. + Auto Complete will be ALT+RIGHT. + Pressing Enter will open the folder. + Back / Context Menu diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 31bc2ba5046..037799cf481 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -171,6 +171,14 @@ Key="{Binding AutoCompleteHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='key'}" Command="{Binding AutocompleteQueryCommand}" Modifiers="{Binding AutoCompleteHotkey2, Converter={StaticResource StringToKeyBindingConverter}, ConverterParameter='modifiers'}" /> + + diff --git a/Flow.Launcher/Resources/Dark.xaml b/Flow.Launcher/Resources/Dark.xaml index 1ec01f8d143..b1f23c4b426 100644 --- a/Flow.Launcher/Resources/Dark.xaml +++ b/Flow.Launcher/Resources/Dark.xaml @@ -65,6 +65,7 @@ + @@ -109,6 +110,12 @@ #ffffff #272727 + + + + + + diff --git a/Flow.Launcher/Resources/Light.xaml b/Flow.Launcher/Resources/Light.xaml index 4d765d161fe..59541c51d4b 100644 --- a/Flow.Launcher/Resources/Light.xaml +++ b/Flow.Launcher/Resources/Light.xaml @@ -22,7 +22,7 @@ - + @@ -102,8 +102,12 @@ #f6f6f6 - - + + + + + + diff --git a/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs index f59b65c1c4c..57ec7d0bdc8 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage3.xaml.cs @@ -12,7 +12,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) Settings = Ioc.Default.GetRequiredService(); // Sometimes the navigation is not triggered by button click, // so we need to reset the page number - Ioc.Default.GetRequiredService().PageNum = 3; + Ioc.Default.GetRequiredService().PageNum = 4; InitializeComponent(); } diff --git a/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs index 4c83f3a83e0..5cedea6e3e3 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage4.xaml.cs @@ -12,7 +12,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) Settings = Ioc.Default.GetRequiredService(); // Sometimes the navigation is not triggered by button click, // so we need to reset the page number - Ioc.Default.GetRequiredService().PageNum = 4; + Ioc.Default.GetRequiredService().PageNum = 5; InitializeComponent(); } diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs index 95d7ff1a0d2..baca1cd96d8 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs @@ -19,7 +19,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) Settings = Ioc.Default.GetRequiredService(); // Sometimes the navigation is not triggered by button click, // so we need to reset the page number - Ioc.Default.GetRequiredService().PageNum = 5; + Ioc.Default.GetRequiredService().PageNum = 6; InitializeComponent(); } diff --git a/Flow.Launcher/Resources/Pages/WelcomePageUserType.xaml b/Flow.Launcher/Resources/Pages/WelcomePageUserType.xaml new file mode 100644 index 00000000000..a6b09302f7f --- /dev/null +++ b/Flow.Launcher/Resources/Pages/WelcomePageUserType.xaml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher/Resources/Pages/WelcomePageUserType.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePageUserType.xaml.cs new file mode 100644 index 00000000000..98fe3dece8c --- /dev/null +++ b/Flow.Launcher/Resources/Pages/WelcomePageUserType.xaml.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; +using System.Windows.Navigation; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.ViewModel; +using System.Windows.Media; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure; +using System.IO; +using System.Text.Json; + +namespace Flow.Launcher.Resources.Pages; + +public class RadioCardData +{ + public string Key { get; set; } + public string Icon { get; set; } + public string Description1 { get; set; } + public string Bullet1 { get; set; } + public string Bullet2 { get; set; } + public string Bullet3 { get; set; } +} +public partial class WelcomePageUserType +{ + public Settings Settings { get; set; } + + public WelcomePageUserType() + { + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + Settings = Ioc.Default.GetRequiredService(); + // Sometimes the navigation is not triggered by button click, + // so we need to reset the page number + Ioc.Default.GetRequiredService().PageNum = 3; + InitializeComponent(); + } + + private void OnStyleChecked(object sender, RoutedEventArgs e) + { + var rb = sender as RadioButton; + if (rb?.Tag is not RadioCardData data) return; + + var settings = Ioc.Default.GetRequiredService(); + + var pluginDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins", "Flow.Launcher.Plugin.Explorer"); + var explorerSettingsPath = Path.Combine(pluginDirectory, "Settings.json"); + + if (File.Exists(explorerSettingsPath)) + { + var explorerSettingsJson = File.ReadAllText(explorerSettingsPath); + var explorerSettings = JsonSerializer.Deserialize>(explorerSettingsJson); + + switch (data.Key) + { + case "CLI": + if (explorerSettings != null) + explorerSettings["UseLocationAsWorkingDir"] = false; + settings.AutoCompleteHotkey = "Tab"; + break; + + case "GUI": + if (explorerSettings != null) + explorerSettings["UseLocationAsWorkingDir"] = true; + settings.AutoCompleteHotkey = $"{KeyConstant.Alt} + Right"; + break; + } + + File.WriteAllText(explorerSettingsPath, JsonSerializer.Serialize(explorerSettings, new JsonSerializerOptions { WriteIndented = true })); + } + + settings.Save(); + } +} + diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index b1d72ede5bf..ddd2ba328e3 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -66,10 +66,9 @@ OnContent="{DynamicResource enable}" /> - @@ -204,15 +203,14 @@ - @@ -227,6 +225,27 @@ + + + + + + + + + /// Command to delete the last word from the query box (for path) + /// + [RelayCommand] + private void DeleteWord() + { + if (string.IsNullOrEmpty(QueryText)) + return; + + string text = QueryText; + // Check if it can be treated as a file path (if it contains a backslash) + if (text.Contains('\\')) + { + // Remove the backslash at the end if it exists + string trimmedPath = text.TrimEnd('\\'); + + // Find the last directory separator (backslash) and cut it off + int lastDirSeparatorIndex = trimmedPath.LastIndexOf('\\'); + + if (lastDirSeparatorIndex > 0) + { + // Cut off the last part of the directory path and keep the backslash + ChangeQueryText(text.Substring(0, lastDirSeparatorIndex + 1)); + } + else if (trimmedPath.EndsWith(":") || trimmedPath.EndsWith(":\\")) + { + // If it is a drive root (e.g. c:\), empty it + ChangeQueryText(string.Empty); + } + else + { + // Handle special cases (if there is a backslash but not a normal path pattern) + DeleteWordNormal(text); + } + } + else + { + // Process as normal text + DeleteWordNormal(text); + } + } + /// + /// Command to delete the last word from the query box (similar to Ctrl+Backspace functionality) + /// + private void DeleteWordNormal(string text) + { + if (string.IsNullOrEmpty(text)) + { + ChangeQueryText(string.Empty); + return; + } + + int length = text.Length; + int currentPos = length - 1; + + // Skip trailing whitespace + while (currentPos >= 0 && char.IsWhiteSpace(text[currentPos])) + currentPos--; + + if (currentPos < 0) + { + ChangeQueryText(string.Empty); + return; + } + + // Check if the current character is a special character + bool IsSpecialChar(char c) => !char.IsLetterOrDigit(c) && c != '_'; + bool isCurrentSpecial = currentPos >= 0 && IsSpecialChar(text[currentPos]); + + // Find the word start position + int wordStart = currentPos; + + if (isCurrentSpecial) + { + // If the current character is a special character, delete only that special character + wordStart--; + } + else + { + // If the current character is a regular character, move to the beginning of the word + while (wordStart >= 0 && !IsSpecialChar(text[wordStart]) && !char.IsWhiteSpace(text[wordStart])) + wordStart--; + } + + // Skip remaining whitespace + if (wordStart >= 0 && char.IsWhiteSpace(text[wordStart])) + { + while (wordStart >= 0 && char.IsWhiteSpace(text[wordStart])) + wordStart--; + } + + // Calculate new text length + int newLength = wordStart + 1; + + // Set new text + if (newLength <= 0) + { + ChangeQueryText(string.Empty); + } + else + { + ChangeQueryText(text[..newLength]); + } + } + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) { var resultsCopy = new List(); @@ -847,6 +954,8 @@ private static string VerifyOrSetDefaultHotkey(string hotkey, string defaultHotk public string PreviewHotkey => VerifyOrSetDefaultHotkey(Settings.PreviewHotkey, "F1"); public string AutoCompleteHotkey => VerifyOrSetDefaultHotkey(Settings.AutoCompleteHotkey, "Ctrl+Tab"); public string AutoCompleteHotkey2 => VerifyOrSetDefaultHotkey(Settings.AutoCompleteHotkey2, ""); + public string DeleteWordHotkey => VerifyOrSetDefaultHotkey(Settings.DeleteWordHotkey, "Alt+Left"); + public string DeleteWordHotkey2 => VerifyOrSetDefaultHotkey(Settings.DeleteWordHotkey2, ""); public string SelectNextItemHotkey => VerifyOrSetDefaultHotkey(Settings.SelectNextItemHotkey, "Tab"); public string SelectNextItemHotkey2 => VerifyOrSetDefaultHotkey(Settings.SelectNextItemHotkey2, ""); public string SelectPrevItemHotkey => VerifyOrSetDefaultHotkey(Settings.SelectPrevItemHotkey, "Shift+Tab"); diff --git a/Flow.Launcher/ViewModel/WelcomeViewModel.cs b/Flow.Launcher/ViewModel/WelcomeViewModel.cs index 5eecabfde85..45c6da53a63 100644 --- a/Flow.Launcher/ViewModel/WelcomeViewModel.cs +++ b/Flow.Launcher/ViewModel/WelcomeViewModel.cs @@ -1,12 +1,49 @@ -using Flow.Launcher.Plugin; +using System; +using Flow.Launcher.Plugin; namespace Flow.Launcher.ViewModel { + public enum WelcomePage + { + Intro = 1, // WelcomePage1 + Features = 2, // WelcomePage2 + UserType = 3, // WelcomePageUserType + Hotkeys = 4, // WelcomePage3 + Commands = 5, // WelcomePage4 + Finish = 6 // WelcomePage5 + } + public partial class WelcomeViewModel : BaseModel { - public const int MaxPageNum = 5; + public const int MaxPageNum = 6; + + public static readonly WelcomePage[] PageSequence = new[] + { + WelcomePage.Intro, + WelcomePage.Features, + WelcomePage.UserType, + WelcomePage.Hotkeys, + WelcomePage.Commands, + WelcomePage.Finish + }; - public string PageDisplay => $"{PageNum}/5"; + public string PageDisplay => $"{GetPageIndex(CurrentPage) + 1}/{PageSequence.Length}"; + + private WelcomePage _currentPage = WelcomePage.Intro; + public WelcomePage CurrentPage + { + get => _currentPage; + set + { + if (_currentPage != value) + { + _currentPage = value; + _pageNum = (int)value; + OnPropertyChanged(); + UpdateView(); + } + } + } private int _pageNum = 1; public int PageNum @@ -17,6 +54,7 @@ public int PageNum if (_pageNum != value) { _pageNum = value; + _currentPage = (WelcomePage)value; OnPropertyChanged(); UpdateView(); } @@ -45,24 +83,18 @@ public bool NextEnabled } } + private int GetPageIndex(WelcomePage page) + { + return Array.IndexOf(PageSequence, page); + } + private void UpdateView() { OnPropertyChanged(nameof(PageDisplay)); - if (PageNum == 1) - { - BackEnabled = false; - NextEnabled = true; - } - else if (PageNum == MaxPageNum) - { - BackEnabled = true; - NextEnabled = false; - } - else - { - BackEnabled = true; - NextEnabled = true; - } + + int index = GetPageIndex(CurrentPage); + BackEnabled = index > 0; + NextEnabled = index < PageSequence.Length - 1; } } } diff --git a/Flow.Launcher/WelcomeWindow.xaml.cs b/Flow.Launcher/WelcomeWindow.xaml.cs index 637f9448d9a..b6054cfdad7 100644 --- a/Flow.Launcher/WelcomeWindow.xaml.cs +++ b/Flow.Launcher/WelcomeWindow.xaml.cs @@ -32,27 +32,26 @@ public WelcomeWindow() private void ForwardButton_Click(object sender, RoutedEventArgs e) { - if (_viewModel.PageNum < WelcomeViewModel.MaxPageNum) + int currentIndex = Array.IndexOf(WelcomeViewModel.PageSequence, _viewModel.CurrentPage); + if (currentIndex < WelcomeViewModel.PageSequence.Length - 1) { - _viewModel.PageNum++; - ContentFrame.Navigate(PageTypeSelector(_viewModel.PageNum), null, _forwardTransitionInfo); - } - else - { - _viewModel.NextEnabled = false; + WelcomePage nextPage = WelcomeViewModel.PageSequence[currentIndex + 1]; + + Type nextPageType = PageTypeSelector(nextPage); + ContentFrame.Navigate(nextPageType, null, _forwardTransitionInfo); + + _viewModel.CurrentPage = nextPage; } } - private void BackwardButton_Click(object sender, RoutedEventArgs e) { - if (_viewModel.PageNum > 1) - { - _viewModel.PageNum--; - ContentFrame.Navigate(PageTypeSelector(_viewModel.PageNum), null, _backTransitionInfo); - } - else + int currentIndex = Array.IndexOf(WelcomeViewModel.PageSequence, _viewModel.CurrentPage); + if (currentIndex > 0) { - _viewModel.BackEnabled = false; + WelcomePage prevPage = WelcomeViewModel.PageSequence[currentIndex - 1]; + Type prevPageType = PageTypeSelector(prevPage); + ContentFrame.Navigate(prevPageType, null, _backTransitionInfo); + _viewModel.CurrentPage = prevPage; } } @@ -61,17 +60,25 @@ private void BtnCancel_OnClick(object sender, RoutedEventArgs e) Close(); } - private static Type PageTypeSelector(int pageNumber) + private static Type PageTypeSelector(WelcomePage page) { - return pageNumber switch + Type result = page switch { - 1 => typeof(WelcomePage1), - 2 => typeof(WelcomePage2), - 3 => typeof(WelcomePage3), - 4 => typeof(WelcomePage4), - 5 => typeof(WelcomePage5), - _ => throw new ArgumentOutOfRangeException(nameof(pageNumber), pageNumber, "Unexpected Page Number") + WelcomePage.Intro => typeof(WelcomePage1), + WelcomePage.Features => typeof(WelcomePage2), + WelcomePage.UserType => typeof(WelcomePageUserType), + WelcomePage.Hotkeys => typeof(WelcomePage3), + WelcomePage.Commands => typeof(WelcomePage4), + WelcomePage.Finish => typeof(WelcomePage5), + _ => throw new ArgumentOutOfRangeException(nameof(page), page, "Unexpected page type") }; + return result; + } + + // This method is used to convert the page number to the corresponding page type. + private static Type PageTypeSelector(int pageNumber) + { + return PageTypeSelector((WelcomePage)pageNumber); } private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ @@ -91,7 +98,7 @@ private void OnActivated(object sender, EventArgs e) private void ContentFrame_Loaded(object sender, RoutedEventArgs e) { - ContentFrame.Navigate(PageTypeSelector(1)); /* Set First Page */ + ContentFrame.Navigate(PageTypeSelector(WelcomePage.Intro)); /* Set First Page */ } private void Window_Closed(object sender, EventArgs e)