refactor: rewrite the histories filter function to supports both include and exclude modes (#690)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo 2024-11-13 21:45:28 +08:00
parent e3ffe3ef6c
commit ca5bc4b4df
No known key found for this signature in database
27 changed files with 767 additions and 309 deletions

View file

@ -24,6 +24,10 @@
<Style Selector="ListBoxItem" x:DataType="vm:BranchTreeNode">
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="ListBoxItem:pointerover v|FilterModeSwitchButton">
<Setter Property="IsNoneVisible" Value="True"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
@ -67,15 +71,10 @@
Foreground="{DynamicResource Brush.BadgeFG}"
Background="{DynamicResource Brush.Badge}"/>
<!-- Filter Toggle Button -->
<ToggleButton Grid.Column="3"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
IsVisible="{Binding IsBranch}"
IsChecked="{Binding IsFiltered}"
Click="OnToggleFilterClicked"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
<!-- Filter Mode Switcher -->
<v:FilterModeSwitchButton Grid.Column="3"
Margin="0,0,8,0"
Mode="{Binding FilterMode}"/>
</Grid>
</Grid>
</DataTemplate>

View file

@ -428,28 +428,6 @@ namespace SourceGit.Views
}
}
private void OnToggleFilterClicked(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.Repository repo &&
sender is ToggleButton toggle &&
toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch } node)
{
bool filtered = toggle.IsChecked == true;
List<string> filters = [branch.FullName];
if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream))
{
filters.Add(branch.Upstream);
node.IsFiltered = filtered;
UpdateUpstreamFilterState(repo.RemoteBranchTrees, branch.Upstream, filtered);
}
repo.UpdateFilters(filters, filtered);
}
e.Handled = true;
}
private void MakeRows(List<ViewModels.BranchTreeNode> rows, List<ViewModels.BranchTreeNode> nodes, int depth)
{
foreach (var node in nodes)
@ -477,23 +455,6 @@ namespace SourceGit.Views
CollectBranchesInNode(outs, sub);
}
private bool UpdateUpstreamFilterState(List<ViewModels.BranchTreeNode> collection, string upstream, bool isFiltered)
{
foreach (var node in collection)
{
if (node.Backend is Models.Branch b && b.FullName == upstream)
{
node.IsFiltered = isFiltered;
return true;
}
if (node.Backend is Models.Remote r && upstream.StartsWith($"refs/remotes/{r.Name}/", StringComparison.Ordinal))
return UpdateUpstreamFilterState(node.Children, upstream, isFiltered);
}
return false;
}
private bool _disableSelectionChangingEvent = false;
}
}

View file

@ -0,0 +1,32 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.FilterModeSwitchButton"
x:Name="ThisControl">
<Button Classes="icon_button"
Width="12" Height="12"
Padding="0"
Background="Transparent"
VerticalContentAlignment="Center"
Click="OnChangeFilterModeButtonClicked">
<Grid>
<Path Width="12" Height="12"
Data="{StaticResource Icons.Eye}"
Fill="{DynamicResource Brush.FG2}"
IsVisible="{Binding #ThisControl.Mode, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:FilterMode.None}}"/>
<Path Width="12" Height="12"
Data="{StaticResource Icons.Filter}"
Fill="{DynamicResource Brush.Accent}"
IsVisible="{Binding #ThisControl.Mode, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:FilterMode.Included}}"/>
<Path Width="12" Height="12"
Data="{StaticResource Icons.EyeClose}"
Fill="{DynamicResource Brush.Accent}"
IsVisible="{Binding #ThisControl.Mode, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:FilterMode.Excluded}}"/>
</Grid>
</Button>
</UserControl>

View file

@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public partial class FilterModeSwitchButton : UserControl
{
public static readonly StyledProperty<Models.FilterMode> ModeProperty =
AvaloniaProperty.Register<FilterModeSwitchButton, Models.FilterMode>(nameof(Mode));
public Models.FilterMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
public static readonly StyledProperty<bool> IsNoneVisibleProperty =
AvaloniaProperty.Register<FilterModeSwitchButton, bool>(nameof(IsNoneVisible));
public bool IsNoneVisible
{
get => GetValue(IsNoneVisibleProperty);
set => SetValue(IsNoneVisibleProperty, value);
}
public static readonly StyledProperty<bool> IsContextMenuOpeningProperty =
AvaloniaProperty.Register<FilterModeSwitchButton, bool>(nameof(IsContextMenuOpening));
public bool IsContextMenuOpening
{
get => GetValue(IsContextMenuOpeningProperty);
set => SetValue(IsContextMenuOpeningProperty, value);
}
public FilterModeSwitchButton()
{
IsVisible = false;
InitializeComponent();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ModeProperty ||
change.Property == IsNoneVisibleProperty ||
change.Property == IsContextMenuOpeningProperty)
{
var visible = (Mode != Models.FilterMode.None || IsNoneVisible || IsContextMenuOpening);
SetCurrentValue(IsVisibleProperty, visible);
}
}
private void OnChangeFilterModeButtonClicked(object sender, RoutedEventArgs e)
{
var repoView = this.FindAncestorOfType<Repository>();
if (repoView == null)
return;
var repo = repoView.DataContext as ViewModels.Repository;
if (repo == null)
return;
var button = sender as Button;
if (button == null)
return;
if (DataContext is Models.Tag tag)
{
var mode = tag.FilterMode;
var none = new MenuItem();
none.Icon = App.CreateMenuIcon("Icons.Eye");
none.Header = App.Text("Repository.FilterCommits.Default");
none.IsEnabled = mode != Models.FilterMode.None;
none.Click += (_, ev) =>
{
UpdateTagFilterMode(repo, tag, Models.FilterMode.None);
ev.Handled = true;
};
var include = new MenuItem();
include.Icon = App.CreateMenuIcon("Icons.Filter");
include.Header = App.Text("Repository.FilterCommits.Include");
include.IsEnabled = mode != Models.FilterMode.Included;
include.Click += (_, ev) =>
{
UpdateTagFilterMode(repo, tag, Models.FilterMode.Included);
ev.Handled = true;
};
var exclude = new MenuItem();
exclude.Icon = App.CreateMenuIcon("Icons.EyeClose");
exclude.Header = App.Text("Repository.FilterCommits.Exclude");
exclude.IsEnabled = mode != Models.FilterMode.Excluded;
exclude.Click += (_, ev) =>
{
UpdateTagFilterMode(repo, tag, Models.FilterMode.Excluded);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(none);
menu.Items.Add(include);
menu.Items.Add(exclude);
if (mode == Models.FilterMode.None)
{
IsContextMenuOpening = true;
menu.Closed += (_, _) => IsContextMenuOpening = false;
}
menu.Open(button);
}
else if (DataContext is ViewModels.BranchTreeNode node)
{
var mode = node.FilterMode;
var none = new MenuItem();
none.Icon = App.CreateMenuIcon("Icons.Eye");
none.Header = App.Text("Repository.FilterCommits.Default");
none.IsEnabled = mode != Models.FilterMode.None;
none.Click += (_, ev) =>
{
UpdateBranchFilterMode(repo, node, Models.FilterMode.None);
ev.Handled = true;
};
var include = new MenuItem();
include.Icon = App.CreateMenuIcon("Icons.Filter");
include.Header = App.Text("Repository.FilterCommits.Include");
include.IsEnabled = mode != Models.FilterMode.Included;
include.Click += (_, ev) =>
{
UpdateBranchFilterMode(repo, node, Models.FilterMode.Included);
ev.Handled = true;
};
var exclude = new MenuItem();
exclude.Icon = App.CreateMenuIcon("Icons.EyeClose");
exclude.Header = App.Text("Repository.FilterCommits.Exclude");
exclude.IsEnabled = mode != Models.FilterMode.Excluded;
exclude.Click += (_, ev) =>
{
UpdateBranchFilterMode(repo, node, Models.FilterMode.Excluded);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(none);
menu.Items.Add(include);
menu.Items.Add(exclude);
if (mode == Models.FilterMode.None)
{
IsContextMenuOpening = true;
menu.Closed += (_, _) => IsContextMenuOpening = false;
}
menu.Open(button);
}
e.Handled = true;
}
private void UpdateTagFilterMode(ViewModels.Repository repo, Models.Tag tag, Models.FilterMode mode)
{
var changed = repo.Settings.UpdateHistoriesFilter(tag.Name, Models.FilterType.Tag, mode);
if (changed)
{
tag.FilterMode = mode;
Task.Run(repo.RefreshCommits);
}
}
private void UpdateBranchFilterMode(ViewModels.Repository repo, ViewModels.BranchTreeNode node, Models.FilterMode mode)
{
var isLocal = node.Path.StartsWith("refs/heads/", StringComparison.Ordinal);
var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch;
var tree = isLocal ? repo.LocalBranchTrees : repo.RemoteBranchTrees;
if (node.Backend is Models.Branch branch)
{
var changed = repo.Settings.UpdateHistoriesFilter(node.Path, type, mode);
if (!changed)
return;
node.FilterMode = mode;
// Try to update its upstream.
if (isLocal && !string.IsNullOrEmpty(branch.Upstream) && mode != Models.FilterMode.Excluded)
{
var upstream = branch.Upstream;
var upstreamNode = FindBranchNode(repo.RemoteBranchTrees, upstream);
if (upstreamNode != null)
{
var canUpdateUpstream = true;
foreach (var filter in repo.Settings.HistoriesFilters)
{
bool matched = false;
if (filter.Type == Models.FilterType.RemoteBranch)
matched = filter.Pattern.Equals(upstream, StringComparison.Ordinal);
else if (filter.Type == Models.FilterType.RemoteBranchFolder)
matched = upstream.StartsWith(filter.Pattern, StringComparison.Ordinal);
if (matched && filter.Mode == Models.FilterMode.Excluded)
{
canUpdateUpstream = false;
break;
}
}
if (canUpdateUpstream)
{
changed = repo.Settings.UpdateHistoriesFilter(upstream, Models.FilterType.RemoteBranch, mode);
if (changed)
upstreamNode.FilterMode = mode;
}
}
}
}
else
{
var changed = repo.Settings.UpdateHistoriesFilter(node.Path, isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder, mode);
if (!changed)
return;
node.FilterMode = mode;
ResetChildrenBranchNodeFilterMode(repo, node, isLocal);
}
var parentType = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder;
var cur = node;
do
{
var lastSepIdx = cur.Path.LastIndexOf('/');
if (lastSepIdx <= 0)
break;
var parentPath = cur.Path.Substring(0, lastSepIdx);
var parent = FindBranchNode(tree, parentPath);
if (parent == null)
break;
repo.Settings.UpdateHistoriesFilter(parent.Path, parentType, Models.FilterMode.None);
parent.FilterMode = Models.FilterMode.None;
cur = parent;
} while (true);
Task.Run(repo.RefreshCommits);
}
private void ResetChildrenBranchNodeFilterMode(ViewModels.Repository repo, ViewModels.BranchTreeNode node, bool isLocal)
{
foreach (var child in node.Children)
{
child.FilterMode = Models.FilterMode.None;
if (child.IsBranch)
{
var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch;
repo.Settings.UpdateHistoriesFilter(child.Path, type, Models.FilterMode.None);
}
else
{
var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder;
repo.Settings.UpdateHistoriesFilter(child.Path, type, Models.FilterMode.None);
ResetChildrenBranchNodeFilterMode(repo, child, isLocal);
}
}
}
private ViewModels.BranchTreeNode FindBranchNode(List<ViewModels.BranchTreeNode> nodes, string path)
{
foreach (var node in nodes)
{
if (node.Path.Equals(path, StringComparison.Ordinal))
return node;
if (path.StartsWith(node.Path, StringComparison.Ordinal))
{
var founded = FindBranchNode(node.Children, path);
if (founded != null)
return founded;
}
}
return null;
}
}
}

View file

@ -577,14 +577,14 @@
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedViewIndex" Converter="{x:Static c:IntConverters.IsZero}"/>
<Binding Path="Settings.Filters.Count" Converter="{x:Static c:IntConverters.IsGreaterThanZero}"/>
<Binding Path="Settings.HistoriesFilters.Count" Converter="{x:Static c:IntConverters.IsGreaterThanZero}"/>
</MultiBinding>
</Border.IsVisible>
<Grid Height="28" ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0" Margin="8,0,0,0" Classes="table_header" Text="{DynamicResource Text.Repository.FilterCommitPrefix}"/>
<TextBlock Grid.Column="0" Margin="8,0,0,0" Classes="table_header" Text="{DynamicResource Text.Repository.FilterCommits.Prefix}"/>
<ItemsControl Grid.Column="1" Margin="8,0,0,0" ItemsSource="{Binding Settings.Filters}">
<ItemsControl Grid.Column="1" Margin="8,0,0,0" ItemsSource="{Binding Settings.HistoriesFilters}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
@ -592,9 +592,17 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Height="20" Margin="0,0,6,0" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="12">
<TextBlock Classes="primary" Text="{Binding Converter={x:Static c:StringConverters.TrimRefsPrefix}}" Margin="8,0"/>
<DataTemplate DataType="m:Filter">
<Border Height="20"
Margin="0,0,6,0"
CornerRadius="12"
BorderThickness="1"
BorderBrush="{Binding Mode, Converter={x:Static c:FilterModeConverters.ToBorderBrush}}">
<StackPanel Orientation="Horizontal" Margin="8,0">
<Path Width="10" Height="10" Data="{StaticResource Icons.Branch}" IsVisible="{Binding IsBranch}"/>
<Path Width="10" Height="10" Data="{StaticResource Icons.Tag}" IsVisible="{Binding !IsBranch}"/>
<TextBlock Classes="primary" Text="{Binding Pattern, Converter={x:Static c:StringConverters.TrimRefsPrefix}}" Margin="4,0,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>

View file

@ -12,6 +12,10 @@
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4"/>
</Style>
<Style Selector="ListBoxItem:pointerover v|FilterModeSwitchButton">
<Setter Property="IsNoneVisible" Value="True"/>
</Style>
</UserControl.Styles>
<UserControl.DataTemplates>
@ -43,15 +47,14 @@
Classes="primary"
Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}"
Margin="8,0,0,0"/>
<ToggleButton Grid.Column="3"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
Click="OnToggleFilterClicked"
IsChecked="{Binding IsFiltered, Mode=TwoWay}"
IsVisible="{Binding !IsFolder}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
<ContentControl Grid.Column="3" Content="{Binding Tag}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:Tag">
<v:FilterModeSwitchButton Mode="{Binding FilterMode}"/>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
@ -60,33 +63,28 @@
<DataTemplate DataType="vm:TagCollectionAsList">
<ListBox Classes="repo_left_content_list"
Margin="12,0,0,0"
Margin="8,0,0,0"
ItemsSource="{Binding Tags}"
SelectionMode="Single"
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Tag">
<Grid ColumnDefinitions="Auto,*,Auto"
<Grid ColumnDefinitions="Auto,*,20"
Background="Transparent"
ContextRequested="OnRowContextRequested"
ToolTip.Tip="{Binding Message}">
<Path Grid.Column="0"
Margin="4,0,0,0"
Margin="8,0,0,0"
Width="12" Height="12"
Data="{StaticResource Icons.Tag}"/>
<TextBlock Grid.Column="1"
Classes="primary"
Text="{Binding Name}"
Margin="8,0,0,0"/>
Margin="8,0,0,0"
TextTrimming="CharacterEllipsis"/>
<ToggleButton Grid.Column="2"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
Click="OnToggleFilterClicked"
IsChecked="{Binding IsFiltered, Mode=TwoWay}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
<v:FilterModeSwitchButton Grid.Column="2" Mode="{Binding FilterMode}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

View file

@ -247,23 +247,6 @@ namespace SourceGit.Views
}
}
private void OnToggleFilterClicked(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo)
{
var target = null as Models.Tag;
if (toggle.DataContext is ViewModels.TagTreeNode node)
target = node.Tag;
else if (toggle.DataContext is Models.Tag tag)
target = tag;
if (target != null)
repo.UpdateFilters([target.Name], toggle.IsChecked == true);
}
e.Handled = true;
}
private void MakeTreeRows(List<ViewModels.TagTreeNode> rows, List<ViewModels.TagTreeNode> nodes)
{
foreach (var node in nodes)