M src/PacketLogger/Models/Filters/FilterProfiles.cs => src/PacketLogger/Models/Filters/FilterProfiles.cs +6 -1
@@ 29,6 29,11 @@ public class FilterProfiles
AllProfiles = new ObservableCollection<FilterProfile>();
SelectableProfiles = new ObservableCollection<FilterProfile>();
+ SelectableProfiles.Add(new FilterProfile(false)
+ {
+ Name = "No profile"
+ });
+
AllProfiles.Add(DefaultProfile);
}
@@ 74,7 79,7 @@ public class FilterProfiles
/// <param name="profile">The profile to add.</param>
public void AddProfile(FilterProfile profile)
{
- SelectableProfiles.Add(profile);
+ SelectableProfiles.Insert(SelectableProfiles.Count - 1, profile);
AllProfiles.Add(profile);
}
M src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterChooseViewModel.cs +72 -2
@@ 7,6 7,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
+using DynamicData;
using DynamicData.Binding;
using PacketLogger.Models.Filters;
using ReactiveUI;
@@ 20,6 21,7 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
{
private FilterProfile _currentProfile = null!;
private IDisposable? _cleanUp;
+ private FilterProfile _noProfile;
/// <summary>
/// Initializes a new instance of the <see cref="FilterChooseViewModel"/> class.
@@ 30,6 32,10 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
RecvFilterSelected = true;
CurrentProfile = currentProfile;
CurrentFilter = CreateSendRecvFilter();
+ _noProfile = new FilterProfile(false)
+ {
+ Name = "No profile"
+ };
}
/// <summary>
@@ 40,9 46,59 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
get => _currentProfile;
set
{
+ if (value is null)
+ {
+ return;
+ }
+
+ if (value.Name == "No profile" && value != _noProfile)
+ {
+ CurrentProfile = _noProfile;
+ return;
+ }
+
+ var lastProfile = value;
+ if (value != _noProfile)
+ {
+ RecvEntryViewModel = new FilterEntryViewModel
+ (
+ value.RecvFilterEntry,
+ (data) =>
+ {
+ CopyCurrentToNoProfile(lastProfile);
+ _noProfile.RecvFilterEntry.Filters.Add(data);
+ CurrentProfile = _noProfile;
+ },
+ (data) =>
+ {
+ CopyCurrentToNoProfile(lastProfile);
+ _noProfile.RecvFilterEntry.Filters.Remove(data);
+ CurrentProfile = _noProfile;
+ }
+ );
+ SendEntryViewModel = new FilterEntryViewModel
+ (
+ value.SendFilterEntry,
+ (data) =>
+ {
+ CopyCurrentToNoProfile(lastProfile);
+ _noProfile.SendFilterEntry.Filters.Add(data);
+ CurrentProfile = _noProfile;
+ },
+ (data) =>
+ {
+ CopyCurrentToNoProfile(lastProfile);
+ _noProfile.SendFilterEntry.Filters.Remove(data);
+ CurrentProfile = _noProfile;
+ }
+ );
+ }
+ else
+ {
+ RecvEntryViewModel = new FilterEntryViewModel(value.RecvFilterEntry, null, null);
+ SendEntryViewModel = new FilterEntryViewModel(value.SendFilterEntry, null, null);
+ }
_currentProfile = value;
- RecvEntryViewModel = new FilterEntryViewModel(_currentProfile.RecvFilterEntry);
- SendEntryViewModel = new FilterEntryViewModel(_currentProfile.SendFilterEntry);
var recvWhenAny = _currentProfile.RecvFilterEntry.WhenAnyPropertyChanged("Active", "Whitelist")
.Subscribe((e) => OnChange());
@@ 62,6 118,20 @@ public class FilterChooseViewModel : ViewModelBase, IDisposable
}
}
+ private void CopyCurrentToNoProfile(FilterProfile lastProfile)
+ {
+ _noProfile.RecvFilterEntry.Filters.Clear();
+ _noProfile.RecvFilterEntry.Filters.AddRange(lastProfile.RecvFilterEntry.Filters);
+
+ _noProfile.SendFilterEntry.Filters.Clear();
+ _noProfile.SendFilterEntry.Filters.AddRange(lastProfile.SendFilterEntry.Filters);
+
+ _noProfile.RecvFilterEntry.Active = lastProfile.RecvFilterEntry.Active;
+ _noProfile.RecvFilterEntry.Whitelist = lastProfile.RecvFilterEntry.Whitelist;
+ _noProfile.SendFilterEntry.Active = lastProfile.SendFilterEntry.Active;
+ _noProfile.SendFilterEntry.Whitelist = lastProfile.SendFilterEntry.Whitelist;
+ }
+
/// <summary>
/// Gets the current recv entry view model.
/// </summary>
M src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterConfigViewModel.cs +2 -2
@@ 22,8 22,8 @@ public class FilterConfigViewModel : ViewModelBase
public FilterConfigViewModel(FilterProfile filterProfile)
{
_filterProfile = filterProfile;
- RecvEntryViewModel = new FilterEntryViewModel(filterProfile.RecvFilterEntry);
- SendEntryViewModel = new FilterEntryViewModel(filterProfile.SendFilterEntry);
+ RecvEntryViewModel = new FilterEntryViewModel(filterProfile.RecvFilterEntry, null, null);
+ SendEntryViewModel = new FilterEntryViewModel(filterProfile.SendFilterEntry, null, null);
}
/// <summary>
M src/PacketLogger/ViewModels/Filters/FilterEntryViewModel.cs => src/PacketLogger/ViewModels/Filters/FilterEntryViewModel.cs +29 -7
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
using System.Reactive;
using PacketLogger.Models.Filters;
using ReactiveUI;
@@ 19,7 20,10 @@ public class FilterEntryViewModel : ViewModelBase
/// Initializes a new instance of the <see cref="FilterEntryViewModel"/> class.
/// </summary>
/// <param name="entry">The profile entry.</param>
- public FilterEntryViewModel(FilterProfileEntry entry)
+ /// <param name="addNew">The action called upon adding new.</param>
+ /// <param name="remove">The action called upon removing the given filter data.</param>
+ public FilterEntryViewModel
+ (FilterProfileEntry entry, Action<FilterCreator.FilterData>? addNew, Action<FilterCreator.FilterData>? remove)
{
NewFilterType = FilterCreator.FilterType.PacketHeader;
Entry = entry;
@@ 30,14 34,24 @@ public class FilterEntryViewModel : ViewModelBase
var selected = SelectedFilter;
if (selected is not null)
{
- var selectedIndex = Entry.Filters.IndexOf(selected);
- SelectedFilter = Entry.Filters.Count > selectedIndex + 1 ? Entry.Filters[selectedIndex + 1] : null;
- if (SelectedFilter is null && selectedIndex > 0)
+ if (remove is not null)
{
- SelectedFilter = Entry.Filters[selectedIndex - 1];
+ SelectedFilter = null;
+ remove(selected);
}
+ else
+ {
+ var selectedIndex = Entry.Filters.IndexOf(selected);
+ SelectedFilter = Entry.Filters.Count > selectedIndex + 1
+ ? Entry.Filters[selectedIndex + 1]
+ : null;
+ if (SelectedFilter is null && selectedIndex > 0)
+ {
+ SelectedFilter = Entry.Filters[selectedIndex - 1];
+ }
- Entry.Filters.Remove(selected);
+ Entry.Filters.Remove(selected);
+ }
}
}
);
@@ 49,7 63,15 @@ public class FilterEntryViewModel : ViewModelBase
if (!string.IsNullOrEmpty(NewFilter))
{
var newFilter = new FilterCreator.FilterData(NewFilterType, NewFilter);
- Entry.Filters.Add(newFilter);
+ if (addNew is not null)
+ {
+ addNew(newFilter);
+ }
+ else
+ {
+ Entry.Filters.Add(newFilter);
+ }
+
NewFilter = string.Empty;
SelectedFilter = newFilter;
}
A src/PacketLogger/ViewModels/Log/PacketLogViewModel.cs => src/PacketLogger/ViewModels/Log/PacketLogViewModel.cs +212 -0
@@ 0,0 1,212 @@
+//
+// PacketLogViewModel.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using Avalonia;
+using DynamicData;
+using DynamicData.Binding;
+using PacketLogger.Models;
+using PacketLogger.Models.Filters;
+using PacketLogger.Models.Packets;
+using PacketLogger.ViewModels.Filters;
+using ReactiveUI;
+
+namespace PacketLogger.ViewModels.Log;
+
+/// <inheritdoc />
+public class PacketLogViewModel : ViewModelBase, IDisposable
+{
+ private readonly FilterProfiles _filterProfiles;
+ private readonly ReadOnlyObservableCollection<PacketInfo> _packets;
+ private readonly IDisposable _cleanUp;
+ private bool _logReceived = true;
+ private bool _logSent = true;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PacketLogViewModel"/> class.
+ /// </summary>
+ /// <param name="packetProvider">The packet provider.</param>
+ /// <param name="filterProfiles">The filter profiles.</param>
+ public PacketLogViewModel(IPacketProvider packetProvider, FilterProfiles filterProfiles)
+ {
+ _filterProfiles = filterProfiles;
+ FilterChoose = new FilterChooseViewModel(new FilterProfile(false));
+ FilterChoose.CurrentProfile = filterProfiles.DefaultFilterEnabled
+ ? filterProfiles.DefaultProfile
+ : new FilterProfile(false)
+ {
+ Name = "No profile"
+ };
+ Provider = packetProvider;
+
+ var dynamicFilter = FilterChoose.WhenValueChanged(x => x.CurrentFilter)
+ .Select
+ (
+ filter =>
+ {
+ return (Func<PacketInfo, bool>)((pi) =>
+ {
+ if (filter is null)
+ {
+ return true;
+ }
+
+ return filter.Match(pi);
+ });
+ }
+ );
+
+ var packetsSubscription = Provider.Packets.Connect()
+ .Filter(dynamicFilter)
+ .Sort(new PacketComparer())
+ .Bind(out _packets)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .DisposeMany()
+ .Subscribe
+ (
+ _ =>
+ {
+ if (Scroll)
+ {
+ RxApp.MainThreadScheduler.Schedule
+ (
+ DateTimeOffset.Now.AddMilliseconds(100),
+ () =>
+ {
+ if (FilteredPackets.Count > 0)
+ {
+ SelectedPacket = FilteredPackets[^1];
+ }
+ }
+ );
+ }
+ }
+ );
+
+ _cleanUp = packetsSubscription;
+ CopyPackets = ReactiveCommand.CreateFromObservable<IList, Unit>
+ (
+ list => Observable.StartAsync
+ (
+ async () =>
+ {
+ var clipboardString = string.Join
+ ('\n', list.OfType<PacketInfo>().Select(x => x.PacketString));
+ await Application.Current!.Clipboard!.SetTextAsync(clipboardString);
+ }
+ )
+ );
+
+ TogglePane = ReactiveCommand.Create<Unit, bool>
+ (
+ _ => PaneOpen = !PaneOpen
+ );
+
+ Clear = ReactiveCommand.Create
+ (
+ () => Provider.Clear()
+ );
+ }
+
+ /// <summary>
+ /// Gets filter profiles.
+ /// </summary>
+ public FilterProfiles Profiles => _filterProfiles;
+
+ /// <summary>
+ /// Gets the filtered packets.
+ /// </summary>
+ public ReadOnlyObservableCollection<PacketInfo> FilteredPackets => _packets;
+
+ /// <summary>
+ /// Gets packet provider.
+ /// </summary>
+ public IPacketProvider Provider { get; }
+
+ /// <summary>
+ /// Gets whether the pane is open.
+ /// </summary>
+ public bool PaneOpen { get; private set; } = true;
+
+ /// <summary>
+ /// Gets the toggle pane command.
+ /// </summary>
+ public ReactiveCommand<Unit, bool> TogglePane { get; }
+
+ /// <summary>
+ /// Gets command to copy packets.
+ /// </summary>
+ public ReactiveCommand<IList, Unit> CopyPackets { get; }
+
+ /// <summary>
+ /// Gets the command for clearing.
+ /// </summary>
+ public ReactiveCommand<Unit, Unit> Clear { get; }
+
+ /// <summary>
+ /// Gets or sets whether to log received packets.
+ /// </summary>
+ public bool LogReceived
+ {
+ get => _logReceived;
+ set
+ {
+ Provider.LogReceived = value;
+ _logReceived = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets whether to log sent packets.
+ /// </summary>
+ public bool LogSent
+ {
+ get => _logSent;
+ set
+ {
+ Provider.LogSent = value;
+ _logSent = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets whether to scroll to teh bottom of the grid.
+ /// </summary>
+ public bool Scroll { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the currently selected packet.
+ /// </summary>
+ public object? SelectedPacket { get; set; }
+
+ /// <summary>
+ /// Gets empty string.
+ /// </summary>
+ public string Empty { get; } = string.Empty;
+
+ /// <summary>
+ /// Gets the filter choose view model.
+ /// </summary>
+ public FilterChooseViewModel FilterChoose { get; }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ TogglePane.Dispose();
+ CopyPackets.Dispose();
+ Clear.Dispose();
+ Provider.Dispose();
+ (Provider as CommsPacketProvider)?.CustomDispose();
+ _cleanUp.Dispose();
+ }
+}<
\ No newline at end of file
A src/PacketLogger/Views/Log/PacketLogView.axaml => src/PacketLogger/Views/Log/PacketLogView.axaml +100 -0
@@ 0,0 1,100 @@
+<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:vm="clr-namespace:PacketLogger.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="PacketLogger.Views.Log.PacketLogView"
+ xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
+ xmlns:converters="clr-namespace:PacketLogger.Converters"
+ xmlns:views="clr-namespace:PacketLogger.Views"
+ xmlns:packets="clr-namespace:PacketLogger.Models.Packets"
+ xmlns:packetLogger="clr-namespace:PacketLogger"
+ xmlns:log="clr-namespace:PacketLogger.ViewModels.Log"
+ x:Name="UserControl">
+ <UserControl.Resources>
+ <converters:PacketSourceConverter x:Key="packetSourceConverter" />
+ </UserControl.Resources>
+ <Design.DataContext>
+ <log:PacketLogViewModel />
+ </Design.DataContext>
+ <SplitView OpenPaneLength="300" IsPaneOpen="{Binding PaneOpen, Mode = TwoWay}" DisplayMode="CompactInline"
+ PanePlacement="Right">
+ <SplitView.Pane>
+ <Grid Width="280" HorizontalAlignment="Left"
+ ColumnDefinitions="*" RowDefinitions="Auto,*,Auto" Margin="10">
+ <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
+ <Button Grid.Row="0" Margin="0,1,0,0" VerticalContentAlignment="Stretch"
+ HorizontalContentAlignment="Stretch" Width="22" Height="22"
+ Command="{Binding TogglePane}">
+ <Grid>
+ <i:Icon Value="mdi-menu-left" Height="22" Width="22" Margin="0,0,2,0"
+ IsVisible="{Binding !PaneOpen}" />
+ <i:Icon Value="mdi-menu-right" Height="22" Width="22" IsVisible="{Binding PaneOpen}" />
+ </Grid>
+ </Button>
+
+ <TextBlock VerticalAlignment="Center" FontSize="30" Text="Filter"
+ Margin="5,0,0,0" />
+
+ <ComboBox Width="160" Margin="10,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Stretch"
+ Items="{Binding Profiles.SelectableProfiles}" SelectedItem="{Binding FilterChoose.CurrentProfile, Mode = TwoWay}">
+ <ComboBox.DataTemplates>
+ <DataTemplate>
+ <TextBlock Text="{Binding Name}" />
+ </DataTemplate>
+ </ComboBox.DataTemplates>
+ </ComboBox>
+ </StackPanel>
+
+ <ContentControl Grid.Row="1" Content="{Binding FilterChoose}" />
+
+ <Grid Grid.Row="2" RowDefinitions="40,40" ColumnDefinitions="140,140">
+ <CheckBox Grid.Row="0" Grid.Column="0" Content="Log received" IsChecked="{Binding LogReceived}" />
+ <CheckBox Grid.Row="0" Grid.Column="1" Content="Log sent" IsChecked="{Binding LogSent}" />
+ <CheckBox Grid.Row="1" Grid.Column="0" Content="Scroll" IsChecked="{Binding Scroll}" />
+ <Button Grid.Row="1" Grid.Column="1" Content="Clear" HorizontalAlignment="Stretch"
+ HorizontalContentAlignment="Center" Command="{Binding Clear}" />
+ </Grid>
+ </Grid>
+ </SplitView.Pane>
+
+ <ListBox Items="{Binding FilteredPackets}"
+ x:Name="PacketsLog"
+ SelectionMode="Multiple"
+ SelectedItem="{Binding SelectedPacket, Mode=TwoWay}"
+ VerticalAlignment="Stretch"
+ SelectionChanged="PacketsLog_OnSelectionChanged">
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Padding" Value="1" />
+ </Style>
+ <Style Selector="TextBlock">
+ <Setter Property="FontSize" Value="12" />
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <StackPanel Height="16" Margin="0" Orientation="Horizontal">
+ <TextBlock Width="60" Text="{Binding Date, StringFormat = {}{0:HH:mm:ss}}" />
+ <TextBlock Width="40"
+ Text="{Binding Source, Converter = {StaticResource packetSourceConverter}}">
+ </TextBlock>
+ <Border ToolTip.Tip="{Binding PacketString}">
+ <TextBlock VerticalAlignment="Center" Text="{Binding PacketString}"
+ TextTrimming="CharacterEllipsis">
+ </TextBlock>
+ </Border>
+ </StackPanel>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ <ListBox.ContextMenu>
+ <ContextMenu Name="PacketMenu">
+ <MenuItem Header="Copy packets" Command="{Binding CopyPackets}"
+ CommandParameter="{Binding ElementName=PacketsLog, Path=SelectedItems}" IsEnabled="True">
+ </MenuItem>
+ </ContextMenu>
+ </ListBox.ContextMenu>
+ </ListBox>
+ </SplitView>
+</UserControl><
\ No newline at end of file
A src/PacketLogger/Views/Log/PacketLogView.axaml.cs => src/PacketLogger/Views/Log/PacketLogView.axaml.cs +37 -0
@@ 0,0 1,37 @@
+//
+// PacketLogView.axaml.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using PropertyChanged;
+
+namespace PacketLogger.Views.Log;
+
+[DoNotNotify]
+public partial class PacketLogView : UserControl
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PacketLogView"/> class.
+ /// </summary>
+ public PacketLogView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void PacketsLog_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (sender is DataGrid dataGrid && dataGrid.SelectedItem is not null)
+ {
+ dataGrid.ScrollIntoView(dataGrid.SelectedItem, dataGrid.Columns.First());
+ }
+ }
+}<
\ No newline at end of file