diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 4ce0a1ad..65b9d325 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -387,6 +387,7 @@ Create new page Open Preferences dialog Switch active workspace + Switch active page REPOSITORY Commit staged changes Commit and push staged changes @@ -429,6 +430,8 @@ Open in Browser ERROR NOTICE + Switch Workspace + Switch Tab Merge Branch Into: Merge Option: @@ -635,7 +638,6 @@ Use relative time in histories View Logs Visit '{0}' in Browser - Switch Workspace WORKTREES Add Worktree Prune diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 1fabffda..a569cd97 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -433,6 +433,7 @@ Открыть в браузере ОШИБКА УВЕДОМЛЕНИЕ + Переключить рабочее место Влить ветку В: Опции слияния: @@ -639,7 +640,6 @@ Использовать относительное время в историях Просмотр журналов Посетить '{0}' в браузере - Переключить рабочее место РАБОЧИЕ КАТАЛОГИ ДОБАВИТЬ РАБОЧИЙ КАТАЛОГ ОБРЕЗАТЬ diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 1fd1a610..1344dfed 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -391,6 +391,7 @@ 新建页面 打开偏好设置面板 切换工作区 + 切换显示页面 仓库页面快捷键 提交暂存区更改 提交暂存区更改并推送 @@ -433,6 +434,8 @@ 在浏览器中访问 出错了 系统提示 + 切换工作区 + 切换页面 合并分支 目标分支 : 合并方式 : @@ -639,7 +642,6 @@ 在提交列表中使用相对时间 查看命令日志 访问远程仓库 '{0}' - 切换工作区 工作树列表 新增工作树 清理 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index eca8214c..a95f7211 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -391,6 +391,7 @@ 新增頁面 開啟偏好設定面板 切換工作區 + 切換目前頁面 存放庫頁面快速鍵 提交暫存區變更 提交暫存區變更並推送 @@ -433,6 +434,8 @@ 在瀏覽器中開啟連結 發生錯誤 系統提示 + 切換工作區 + 切換目前頁面 合併分支 目標分支: 合併方式: @@ -639,7 +642,6 @@ 在提交列表中使用相對時間 檢視 Git 指令記錄 檢視遠端存放庫 '{0}' - 切換工作區 工作區列表 新增工作區 清理 diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 9a54bb32..3b6a4dd8 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -23,12 +23,6 @@ namespace SourceGit.ViewModels private set; } - public WorkspaceSwitcher WorkspaceSwitcher - { - get => _workspaceSwitcher; - set => SetProperty(ref _workspaceSwitcher, value); - } - public Workspace ActiveWorkspace { get => _activeWorkspace; @@ -50,6 +44,12 @@ namespace SourceGit.ViewModels } } + public object Switcher + { + get => _switcher; + set => SetProperty(ref _switcher, value); + } + public Launcher(string startupRepo) { _ignoreIndexChange = true; @@ -138,12 +138,17 @@ namespace SourceGit.ViewModels public void OpenWorkspaceSwitcher() { - WorkspaceSwitcher = new WorkspaceSwitcher(this); + Switcher = new WorkspaceSwitcher(this); } - public void CancelWorkspaceSwitcher() + public void OpenTabSwitcher() { - WorkspaceSwitcher = null; + Switcher = new LauncherPageSwitcher(this); + } + + public void CancelSwitcher() + { + Switcher = null; } public void SwitchWorkspace(Workspace to) @@ -618,6 +623,6 @@ namespace SourceGit.ViewModels private LauncherPage _activePage = null; private bool _ignoreIndexChange = false; private string _title = string.Empty; - private WorkspaceSwitcher _workspaceSwitcher = null; + private object _switcher = null; } } diff --git a/src/ViewModels/LauncherPageSwitcher.cs b/src/ViewModels/LauncherPageSwitcher.cs new file mode 100644 index 00000000..b0dfaca3 --- /dev/null +++ b/src/ViewModels/LauncherPageSwitcher.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class LauncherPageSwitcher : ObservableObject + { + public List VisiblePages + { + get => _visiblePages; + private set => SetProperty(ref _visiblePages, value); + } + + public string SearchFilter + { + get => _searchFilter; + set + { + if (SetProperty(ref _searchFilter, value)) + UpdateVisiblePages(); + } + } + + public LauncherPage SelectedPage + { + get => _selectedPage; + set => SetProperty(ref _selectedPage, value); + } + + public LauncherPageSwitcher(Launcher launcher) + { + _launcher = launcher; + UpdateVisiblePages(); + } + + public void ClearFilter() + { + SearchFilter = string.Empty; + } + + public void Switch() + { + if (_selectedPage is { }) + _launcher.ActivePage = _selectedPage; + + _launcher.CancelSwitcher(); + } + + private void UpdateVisiblePages() + { + var visible = new List(); + if (string.IsNullOrEmpty(_searchFilter)) + { + visible.AddRange(_launcher.Pages); + } + else + { + foreach (var page in _launcher.Pages) + { + if (page.Node.Name.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase) || + (page.Node.IsRepository && page.Node.Id.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase))) + { + visible.Add(page); + } + } + } + + VisiblePages = visible; + } + + private Launcher _launcher = null; + private List _visiblePages = []; + private string _searchFilter = string.Empty; + private LauncherPage _selectedPage = null; + } +} diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index d24f6fbf..a3f63251 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2632,7 +2632,7 @@ namespace SourceGit.ViewModels if (node.Path.Equals(path, StringComparison.Ordinal)) return node; - if (path!.StartsWith(node.Path, StringComparison.Ordinal)) + if (path.StartsWith(node.Path, StringComparison.Ordinal)) { var founded = FindBranchNode(node.Children, path); if (founded != null) diff --git a/src/ViewModels/WorkspaceSwitcher.cs b/src/ViewModels/WorkspaceSwitcher.cs index 01d62744..41f47631 100644 --- a/src/ViewModels/WorkspaceSwitcher.cs +++ b/src/ViewModels/WorkspaceSwitcher.cs @@ -44,7 +44,7 @@ namespace SourceGit.ViewModels if (_selectedWorkspace is { }) _launcher.SwitchWorkspace(_selectedWorkspace); - _launcher.CancelWorkspaceSwitcher(); + _launcher.CancelSwitcher(); } private void UpdateVisibleWorkspaces() diff --git a/src/Views/Hotkeys.axaml b/src/Views/Hotkeys.axaml index 5d9a6f9f..5275f264 100644 --- a/src/Views/Hotkeys.axaml +++ b/src/Views/Hotkeys.axaml @@ -45,8 +45,8 @@ FontSize="{Binding Source={x:Static vm:Preferences.Instance}, Path=DefaultFontSize, Converter={x:Static c:DoubleConverters.Increase}}" Margin="0,0,0,8"/> - - + + @@ -55,7 +55,7 @@ - + @@ -70,8 +70,11 @@ - + + + + + IsVisible="{Binding Switcher, Converter={x:Static ObjectConverters.IsNotNull}}"> - + + + + + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 02cc4f08..abcbaba9 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -133,8 +133,8 @@ namespace SourceGit.Views return; } - // Ctrl+Shift+P opens preference dialog (macOS use hotkeys in system menu bar) - if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: (KeyModifiers.Control | KeyModifiers.Shift), Key: Key.P }) + // Ctrl+, opens preference dialog (macOS use hotkeys in system menu bar) + if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: KeyModifiers.Control, Key: Key.OemComma }) { App.ShowWindow(new Preferences(), true); e.Handled = true; @@ -149,7 +149,7 @@ namespace SourceGit.Views } // Ctrl+Q quits the application (macOS use hotkeys in system menu bar) - if (!OperatingSystem.IsMacOS() && e.KeyModifiers == KeyModifiers.Control && e.Key == Key.Q) + if (!OperatingSystem.IsMacOS() && e is { KeyModifiers: KeyModifiers.Control, Key: Key.Q }) { App.Quit(0); return; @@ -157,10 +157,18 @@ namespace SourceGit.Views if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) { - if (e.Key == Key.P) + if (e.KeyModifiers.HasFlag(KeyModifiers.Shift) && e.Key == Key.P) { vm.OpenWorkspaceSwitcher(); e.Handled = true; + return; + } + + if (e.Key == Key.P) + { + vm.OpenTabSwitcher(); + e.Handled = true; + return; } if (e.Key == Key.W) @@ -257,7 +265,7 @@ namespace SourceGit.Views else if (e.Key == Key.Escape) { vm.ActivePage.CancelPopup(); - vm.CancelWorkspaceSwitcher(); + vm.CancelSwitcher(); e.Handled = true; return; } diff --git a/src/Views/LauncherTabsSelector.axaml b/src/Views/LauncherPageSwitcher.axaml similarity index 70% rename from src/Views/LauncherTabsSelector.axaml rename to src/Views/LauncherPageSwitcher.axaml index 109a2ce7..09f42038 100644 --- a/src/Views/LauncherTabsSelector.axaml +++ b/src/Views/LauncherPageSwitcher.axaml @@ -3,19 +3,27 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="SourceGit.Views.LauncherTabsSelector" - x:Name="ThisControl"> - - + + + + + VerticalContentAlignment="Center" + v:AutoFocusBehaviour.IsEnabled="True"> - + ItemsSource="{Binding VisiblePages, Mode=OneWay}" + SelectedItem="{Binding SelectedPage, Mode=TwoWay}"> @@ -72,30 +83,28 @@ - + - - diff --git a/src/Views/LauncherPageSwitcher.axaml.cs b/src/Views/LauncherPageSwitcher.axaml.cs new file mode 100644 index 00000000..277eb549 --- /dev/null +++ b/src/Views/LauncherPageSwitcher.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class LauncherPageSwitcher : UserControl + { + public LauncherPageSwitcher() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + if (e.Key == Key.Enter && DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnItemDoubleTapped(object sender, TappedEventArgs e) + { + if (DataContext is ViewModels.LauncherPageSwitcher switcher) + { + switcher.Switch(); + e.Handled = true; + } + } + + private void OnSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && PagesListBox.ItemCount > 0) + { + PagesListBox.Focus(NavigationMethod.Directional); + + if (PagesListBox.SelectedIndex < 0) + PagesListBox.SelectedIndex = 0; + else if (PagesListBox.SelectedIndex < PagesListBox.ItemCount) + PagesListBox.SelectedIndex++; + + e.Handled = true; + } + } + } +} + diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 0376e259..f770a5d9 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -40,7 +40,7 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index 12bca91f..b75d93d6 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -1,6 +1,7 @@ using System; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -18,6 +19,20 @@ namespace SourceGit.Views get => GetValue(IsScrollerVisibleProperty); set => SetValue(IsScrollerVisibleProperty, value); } + + public static readonly StyledProperty SearchFilterProperty = + AvaloniaProperty.Register(nameof(SearchFilter)); + + public string SearchFilter + { + get => GetValue(SearchFilterProperty); + set => SetValue(SearchFilterProperty, value); + } + + public AvaloniaList SelectablePages + { + get; + } = []; public LauncherTabBar() { @@ -125,7 +140,15 @@ namespace SourceGit.Views var stroke = new Pen(this.FindResource("Brush.Border0") as IBrush); context.DrawGeometry(fill, stroke, geo); } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == SearchFilterProperty) + UpdateSelectablePages(); + } + private void ScrollTabs(object _, PointerWheelEventArgs e) { if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) @@ -247,16 +270,92 @@ namespace SourceGit.Views e.Handled = true; } - - private void OnGotoSelectedPage(object sender, LauncherTabSelectedEventArgs e) + + private void OnTabsDropdownOpened(object sender, EventArgs e) { - if (DataContext is ViewModels.Launcher vm) - vm.ActivePage = e.Page; + UpdateSelectablePages(); + } + + private void OnTabsDropdownKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + else if (e.Key == Key.Enter) + { + if (TabsDropdownList.SelectedItem is ViewModels.LauncherPage page && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + } + + private void OnTabsDropdownSearchBoxKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Down && TabsDropdownList.ItemCount > 0) + { + TabsDropdownList.Focus(NavigationMethod.Directional); - PageSelector.Flyout?.Hide(); - e.Handled = true; + if (TabsDropdownList.SelectedIndex < 0) + TabsDropdownList.SelectedIndex = 0; + else if (TabsDropdownList.SelectedIndex < TabsDropdownList.ItemCount) + TabsDropdownList.SelectedIndex++; + + e.Handled = true; + } } + private void OnTabsDropdownLostFocus(object sender, RoutedEventArgs e) + { + if (sender is Control { IsFocused: false, IsKeyboardFocusWithin: false }) + PageSelector.Flyout?.Hide(); + } + + private void OnClearSearchFilter(object sender, RoutedEventArgs e) + { + SearchFilter = string.Empty; + } + + private void OnTabsDropdownItemDoubleTapped(object sender, TappedEventArgs e) + { + if (sender is Control { DataContext: ViewModels.LauncherPage page } && + DataContext is ViewModels.Launcher vm) + { + vm.ActivePage = page; + PageSelector.Flyout?.Hide(); + e.Handled = true; + } + } + + private void UpdateSelectablePages() + { + if (DataContext is not ViewModels.Launcher vm) + return; + + SelectablePages.Clear(); + + var pages = vm.Pages; + var filter = SearchFilter?.Trim() ?? ""; + if (string.IsNullOrEmpty(filter)) + { + SelectablePages.AddRange(pages); + return; + } + + foreach (var page in pages) + { + var node = page.Node; + if (node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + (node.IsRepository && node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase))) + SelectablePages.Add(page); + } + } + private bool _pressedTab = false; private Point _pressedTabPosition = new Point(); private bool _startDragTab = false; diff --git a/src/Views/LauncherTabsSelector.axaml.cs b/src/Views/LauncherTabsSelector.axaml.cs deleted file mode 100644 index 61d7a966..00000000 --- a/src/Views/LauncherTabsSelector.axaml.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Interactivity; - -namespace SourceGit.Views -{ - public class LauncherTabSelectedEventArgs : RoutedEventArgs - { - public ViewModels.LauncherPage Page { get; } - - public LauncherTabSelectedEventArgs(ViewModels.LauncherPage page) - { - RoutedEvent = LauncherTabsSelector.PageSelectedEvent; - Page = page; - } - } - - public partial class LauncherTabsSelector : UserControl - { - public static readonly StyledProperty> PagesProperty = - AvaloniaProperty.Register>(nameof(Pages)); - - public AvaloniaList Pages - { - get => GetValue(PagesProperty); - set => SetValue(PagesProperty, value); - } - - public static readonly StyledProperty SearchFilterProperty = - AvaloniaProperty.Register(nameof(SearchFilter)); - - public string SearchFilter - { - get => GetValue(SearchFilterProperty); - set => SetValue(SearchFilterProperty, value); - } - - public static readonly RoutedEvent PageSelectedEvent = - RoutedEvent.Register(nameof(PageSelected), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - public event EventHandler PageSelected - { - add { AddHandler(PageSelectedEvent, value); } - remove { RemoveHandler(PageSelectedEvent, value); } - } - - public AvaloniaList VisiblePages - { - get; - private set; - } - - public LauncherTabsSelector() - { - VisiblePages = new AvaloniaList(); - InitializeComponent(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == PagesProperty || change.Property == SearchFilterProperty) - UpdateVisiblePages(); - } - - private void OnClearSearchFilter(object sender, RoutedEventArgs e) - { - SearchFilter = string.Empty; - } - - private void OnPageSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is ListBox { SelectedItem: ViewModels.LauncherPage page }) - { - _isProcessingSelection = true; - RaiseEvent(new LauncherTabSelectedEventArgs(page)); - _isProcessingSelection = false; - } - - e.Handled = true; - } - - private void UpdateVisiblePages() - { - if (_isProcessingSelection) - return; - - VisiblePages.Clear(); - - if (Pages == null) - return; - - var filter = SearchFilter?.Trim() ?? ""; - if (string.IsNullOrEmpty(filter)) - { - foreach (var p in Pages) - VisiblePages.Add(p); - - return; - } - - foreach (var page in Pages) - { - if (!page.Node.IsRepository) - continue; - - if (page.Node.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || - page.Node.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) - VisiblePages.Add(page); - } - } - - private bool _isProcessingSelection = false; - } -} - diff --git a/src/Views/WorkspaceSwitcher.axaml b/src/Views/WorkspaceSwitcher.axaml index 49fed451..aa621b73 100644 --- a/src/Views/WorkspaceSwitcher.axaml +++ b/src/Views/WorkspaceSwitcher.axaml @@ -9,7 +9,7 @@ x:DataType="vm:WorkspaceSwitcher"> @@ -82,7 +82,7 @@ - +