From e12f03d75b98f3416048b612a8bb54bb904e6551 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Tue, 14 Feb 2023 20:59:22 +0100 Subject: [PATCH] feat: add title generator that reacts to new tabs with same title --- .../Models/Packets/ClientPacketProvider.cs | 5 + .../Models/Packets/DummyPacketProvider.cs | 5 + .../Models/Packets/FilePacketProvider.cs | 5 + .../Models/Packets/IPacketProvider.cs | 6 + .../Models/Titles/NumberedTitleGenerator.cs | 180 ++++++++++++++++++ src/PacketLogger/ViewModels/DockFactory.cs | 12 +- .../ViewModels/DocumentViewModel.cs | 97 +++++++--- src/PacketLogger/Views/DocumentView.axaml | 4 +- src/PacketLogger/Views/MainWindow.axaml | 2 +- 9 files changed, 280 insertions(+), 36 deletions(-) create mode 100644 src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs diff --git a/src/PacketLogger/Models/Packets/ClientPacketProvider.cs b/src/PacketLogger/Models/Packets/ClientPacketProvider.cs index 58df879..2497f85 100644 --- a/src/PacketLogger/Models/Packets/ClientPacketProvider.cs +++ b/src/PacketLogger/Models/Packets/ClientPacketProvider.cs @@ -54,6 +54,11 @@ public abstract class ClientPacketProvider : ReactiveObject, IPacketProvider ? _process.BrowserManager.PlayerManager.Get().Player.Name : null) ?? $"Not in game ({_process.Process.Id})"; + /// + /// Gets or sets title of document. + /// + public string DocumentTitle { get; set; } = string.Empty; + /// public abstract bool IsOpen { get; } diff --git a/src/PacketLogger/Models/Packets/DummyPacketProvider.cs b/src/PacketLogger/Models/Packets/DummyPacketProvider.cs index bd8d578..2467207 100644 --- a/src/PacketLogger/Models/Packets/DummyPacketProvider.cs +++ b/src/PacketLogger/Models/Packets/DummyPacketProvider.cs @@ -38,6 +38,11 @@ public class DummyPacketProvider : IPacketProvider, IDisposable /// public event PropertyChangedEventHandler? PropertyChanged; + /// + /// Gets or sets title of document. + /// + public string DocumentTitle { get; set; } = string.Empty; + /// public bool LogReceived { diff --git a/src/PacketLogger/Models/Packets/FilePacketProvider.cs b/src/PacketLogger/Models/Packets/FilePacketProvider.cs index e94de35..c8c38f9 100644 --- a/src/PacketLogger/Models/Packets/FilePacketProvider.cs +++ b/src/PacketLogger/Models/Packets/FilePacketProvider.cs @@ -39,6 +39,11 @@ public class FilePacketProvider : IPacketProvider _fileName = fileName; } + /// + /// Gets or sets title of document. + /// + public string DocumentTitle { get; set; } = string.Empty; + /// public string Name => Path.GetFileName(_fileName); diff --git a/src/PacketLogger/Models/Packets/IPacketProvider.cs b/src/PacketLogger/Models/Packets/IPacketProvider.cs index 0d09065..2424d73 100644 --- a/src/PacketLogger/Models/Packets/IPacketProvider.cs +++ b/src/PacketLogger/Models/Packets/IPacketProvider.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using DynamicData; using PacketLogger.Models.Filters; +using PacketLogger.Models.Titles; using Remora.Results; namespace PacketLogger.Models.Packets; @@ -26,6 +27,11 @@ public interface IPacketProvider : INotifyPropertyChanged, IDisposable /// public string Name { get; } + /// + /// Gets title of document, if any. + /// + public string DocumentTitle { get; set; } + /// /// Gets whether was called and successfully finished. /// diff --git a/src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs b/src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs new file mode 100644 index 0000000..f800051 --- /dev/null +++ b/src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs @@ -0,0 +1,180 @@ +// +// NumberedTitleGenerator.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.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Threading; + +namespace PacketLogger.Models.Titles; + +/// +/// A generator and manager of document titles. +/// +public class NumberedTitleGenerator +{ + private readonly SemaphoreSlim _semaphore; + private readonly ConcurrentDictionary> _titles; + + /// + /// Initializes a new instance of the class. + /// + public NumberedTitleGenerator() + { + _semaphore = new SemaphoreSlim(1, 1); + _titles = new ConcurrentDictionary>(); + } + + /// + /// Add title with given information. + /// + /// The function used for setting new document title. + /// The observable observing changes to the default, unnumbered title. + /// The current initial title. + /// The handle, title will be removed upon disposal. + public TitleHandle AddTitle + ( + Action setDocumentTitle, + IObservable titleChanged, + string initialTitle + ) + { + var title = new Title + ( + setDocumentTitle, + initialTitle + ); + + title.TitleChanged = titleChanged.Subscribe + ( + newTitle => HandleTitleChange(title, newTitle) + ); + HandleTitleChange(title, title.CurrentTitle); + + return new TitleHandle(this, title); + } + + private void RemoveTitle(Title title) + { + _titles.AddOrUpdate + ( + title.CurrentTitle, + _ => new List(), + (_, u) => + { + u.Remove(title); + return u; + } + ); + UpdateNumbers(title.CurrentTitle); + } + + private void HandleTitleChange(Title title, string newTitle) + { + _semaphore.Wait(); + _titles.AddOrUpdate + ( + title.CurrentTitle, + _ => new List<Title>(), + (_, u) => + { + u.Remove(title); + return u; + } + ); + UpdateNumbers(title.CurrentTitle); + + title.CurrentTitle = newTitle; + _titles.TryAdd(newTitle, new List<Title>()); + _titles.AddOrUpdate + ( + newTitle, + _ => new List<Title>(), + (_, u) => + { + u.Add(title); + return u; + } + ); + UpdateNumbers(title.CurrentTitle); + _semaphore.Release(); + } + + private void UpdateNumbers(string title) + { + if (_titles.TryGetValue(title, out var titles)) + { + if (titles.Count == 1) + { + titles[0].CurrentNumber = 0; + titles[0].SetDocumentTitle(titles[0].CurrentTitle); + } + else if (titles.Count > 1) + { + titles[0].CurrentNumber = null; + titles[0].SetDocumentTitle(titles[0].CurrentTitle); + + for (int i = 1; i < titles.Count; i++) + { + titles[i].CurrentNumber = i; + titles[i].SetDocumentTitle($"{titles[i].CurrentTitle} ({i})"); + } + } + } + } + + /// <summary> + /// A store of a title. + /// </summary> + public class TitleHandle : IDisposable + { + private readonly NumberedTitleGenerator _titleGenerator; + private readonly Title _title; + + /// <summary> + /// Initializes a new instance of the <see cref="TitleHandle"/> class. + /// </summary> + /// <param name="titleGenerator">The title generator.</param> + /// <param name="title">The title.</param> + public TitleHandle(NumberedTitleGenerator titleGenerator, Title title) + { + _titleGenerator = titleGenerator; + _title = title; + } + + /// <inheritdoc /> + public void Dispose() + { + _title.TitleChanged.Dispose(); + _titleGenerator.RemoveTitle(_title); + } + } + + /// <summary> + /// A title. + /// </summary> + /// <param name="SetDocumentTitle">The function used for setting the title of document.</param> + /// <param name="CurrentTitle">The current title.</param> + public record Title(Action<string> SetDocumentTitle, string CurrentTitle) + { + /// <summary> + /// Gets or sets the current suffix. + /// </summary> + public int? CurrentNumber { get; set; } + + /// <summary> + /// Gets or sets the disposable title changed observer. + /// </summary> + public IDisposable TitleChanged { get; set; } = null!; + + /// <summary> + /// Gets or sets the current title. + /// </summary> + public string CurrentTitle { get; set; } = CurrentTitle; + } +} \ No newline at end of file diff --git a/src/PacketLogger/ViewModels/DockFactory.cs b/src/PacketLogger/ViewModels/DockFactory.cs index f54ae33..24162e9 100644 --- a/src/PacketLogger/ViewModels/DockFactory.cs +++ b/src/PacketLogger/ViewModels/DockFactory.cs @@ -18,6 +18,7 @@ using NosSmooth.Core.Stateful; using PacketLogger.Models; using PacketLogger.Models.Filters; using PacketLogger.Models.Packets; +using PacketLogger.Models.Titles; using PacketLogger.Views; using ReactiveUI; using HostWindow = PacketLogger.Views.HostWindow; @@ -35,6 +36,7 @@ public class DockFactory : Factory, IDisposable private readonly ObservableCollection<IPacketProvider> _providers; private readonly NostaleProcesses _processes; private readonly CommsInjector _injector; + private readonly NumberedTitleGenerator _titleGenerator; private IRootDock? _rootDock; private IDocumentDock? _documentDock; @@ -58,6 +60,7 @@ public class DockFactory : Factory, IDisposable StatefulRepository repository ) { + _titleGenerator = new NumberedTitleGenerator(); _services = services; _filterProfiles = filterProfiles; _providers = providers; @@ -111,10 +114,11 @@ public class DockFactory : Factory, IDisposable _repository, _providers, _processes, + _titleGenerator, OnDocumentLoaded, OnDocumentClosed ) - { Id = $"New tab", Title = $"New tab" }; + { Id = Guid.NewGuid().ToString() }; var observable = load(document); observable.Subscribe @@ -155,10 +159,11 @@ public class DockFactory : Factory, IDisposable _repository, _providers, _processes, + _titleGenerator, OnDocumentLoaded, OnDocumentClosed ) - { Id = $"New tab {index}", Title = $"New tab {index}" }; + { Id = $"New tab {index}" }; AddDockable(documentDock, document); SetActiveDockable(document); @@ -179,10 +184,11 @@ public class DockFactory : Factory, IDisposable _repository, _providers, _processes, + _titleGenerator, OnDocumentLoaded, OnDocumentClosed ) - { Id = $"New tab", Title = $"New tab" }; + { Id = $"New tab" }; var documentDock = CreateDocumentDock(); documentDock.IsCollapsable = false; documentDock.ActiveDockable = initialTab; diff --git a/src/PacketLogger/ViewModels/DocumentViewModel.cs b/src/PacketLogger/ViewModels/DocumentViewModel.cs index 6ccf598..38a1b81 100644 --- a/src/PacketLogger/ViewModels/DocumentViewModel.cs +++ b/src/PacketLogger/ViewModels/DocumentViewModel.cs @@ -30,6 +30,7 @@ using NosSmooth.Pcap; using PacketLogger.Models; using PacketLogger.Models.Filters; using PacketLogger.Models.Packets; +using PacketLogger.Models.Titles; using PacketLogger.ViewModels.Log; using PacketLogger.ViewModels.Sender; using PacketLogger.ViewModels.Settings; @@ -47,6 +48,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable private CancellationTokenSource _ctSource; private IPacketProvider? _packetProvider; private IDisposable? _cleanUp; + private NumberedTitleGenerator.TitleHandle _titleHandle; /// <summary> /// Initializes a new instance of the <see cref="DocumentViewModel"/> class. @@ -57,6 +59,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable /// <param name="repository">The repository.</param> /// <param name="providers">The providers.</param> /// <param name="processes">The NosTale processes collection.</param> + /// <param name="titleGenerator">The title generator.</param> /// <param name="onDocumentLoaded">The action to call on loaded.</param> /// <param name="onDocumentUnloaded">The action to call on document unloaded/closed.</param> public DocumentViewModel @@ -67,10 +70,28 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable StatefulRepository repository, ObservableCollection<IPacketProvider> providers, NostaleProcesses processes, + NumberedTitleGenerator titleGenerator, Action<DocumentViewModel> onDocumentLoaded, Action<DocumentViewModel> onDocumentUnloaded ) { + _titleHandle = titleGenerator.AddTitle + ( + title => Title = title, + Observable.Empty<string>(), + "New tab" + ); + + _cleanUp = this.WhenAnyValue(x => x.Title) + .Subscribe(title => + { + if (_packetProvider is not null) + { + _packetProvider.DocumentTitle = title; + } + } + ); + FilterProfiles = filterProfiles; _ctSource = new CancellationTokenSource(); _providers = providers; @@ -84,6 +105,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable { Loading = true; _packetProvider = new DummyPacketProvider(Title); + _packetProvider.DocumentTitle = Title; NestedViewModel = new PacketLogViewModel(_packetProvider, filterProfiles); Loaded = true; onDocumentLoaded(this); @@ -121,7 +143,14 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable return; } - Title = Path.GetFileName(path); + _titleHandle?.Dispose(); + _titleHandle = titleGenerator.AddTitle + ( + title => Title = title, + Observable.Empty<string>(), + Path.GetFileName(path) + ); + _packetProvider.DocumentTitle = Title; NestedViewModel = new PacketLogViewModel(provider, filterProfiles); Loaded = true; Loading = false; @@ -168,21 +197,16 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable return; } - _cleanUp = process.WhenPropertyChanged(x => x.CharacterString) - .ObserveOn(RxApp.MainThreadScheduler) - .Do - ( - _ => - { - Title = (process.BrowserManager.IsInGame.Get() - ? process.BrowserManager.PlayerManager.Get().Player.Name - : null) ?? $"Not in game ({process.Process.Id})"; - } - ) - .Subscribe(); + _titleHandle?.Dispose(); + _titleHandle = titleGenerator.AddTitle + ( + title => Title = title, + provider.WhenAnyValue(x => x.Name).ObserveOn(RxApp.MainThreadScheduler), + provider.Name + ); + _packetProvider.DocumentTitle = Title; NestedViewModel = new PacketLogViewModel(provider, filterProfiles); - Title = handshakeResponse.CharacterName ?? $"Not in game ({process.Process.Id})"; Loading = false; Loaded = true; onDocumentLoaded(this); @@ -209,23 +233,20 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable _packetProvider = provider; repository.SetEntity<ClientPacketProvider>(client, provider); - _cleanUp = process.WhenPropertyChanged(x => x.CharacterString) - .ObserveOn(RxApp.MainThreadScheduler) - .Do - ( - _ => - { - Title = (process.BrowserManager.IsInGame.Get() - ? process.BrowserManager.PlayerManager.Get().Player.Name - : null) ?? $"Not in game ({process.Process.Id})"; - } - ) - .Subscribe(); - + _titleHandle?.Dispose(); + _titleHandle = titleGenerator.AddTitle + ( + title => Title = title, + provider + .WhenAnyValue(x => x.Name) + .ObserveOn(RxApp.MainThreadScheduler) + .Select(x => x + " - sniff"), + provider.Name + ); await provider.Open(); + _packetProvider.DocumentTitle = Title; NestedViewModel = new PacketLogViewModel(provider, filterProfiles); - Title = provider.Name; Loading = false; Loaded = true; onDocumentLoaded(this); @@ -238,7 +259,16 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable { Loading = true; NestedViewModel = new PacketSenderViewModel(provider); - Title = $"Sender ({provider.Name})"; + _titleHandle?.Dispose(); + _titleHandle = titleGenerator.AddTitle + ( + title => Title = title, + provider + .WhenAnyValue(x => x.DocumentTitle) + .ObserveOn(RxApp.MainThreadScheduler) + .Select(x => $"Sender - {provider.DocumentTitle}"), + provider.Name + ); Loaded = true; Loading = false; } @@ -250,7 +280,13 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable ( () => { - Title = "Settings"; + _titleHandle?.Dispose(); + _titleHandle = titleGenerator.AddTitle + ( + title => Title = title, + Observable.Empty<string>(), + "Settings" + ); NestedViewModel = new SettingsViewModel(filterProfiles); Loaded = true; } @@ -354,6 +390,7 @@ public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable /// <inheritdoc /> public void Dispose() { + _titleHandle?.Dispose(); _cleanUp?.Dispose(); _ctSource.Cancel(); _ctSource.Dispose(); diff --git a/src/PacketLogger/Views/DocumentView.axaml b/src/PacketLogger/Views/DocumentView.axaml index 38d58fd..bbb9f1c 100644 --- a/src/PacketLogger/Views/DocumentView.axaml +++ b/src/PacketLogger/Views/DocumentView.axaml @@ -12,7 +12,7 @@ </Design.DataContext> <Grid> - <Border Grid.Row="1" IsVisible="{Binding !Loaded}" + <Border IsVisible="{Binding !Loaded}" MaxWidth="1000" MaxHeight="600" CornerRadius="25" Background="{DynamicResource SystemControlPageBackgroundChromeLowBrush}"> @@ -116,7 +116,7 @@ <TextBlock FontSize="30" Margin="0,0,0,5" Text="Open a sender for" /> <DataGrid Margin="0,0,30,0" Items="{Binding Providers}"> <DataGrid.Columns> - <DataGridTextColumn Header="Tab" Binding="{Binding Name}" /> + <DataGridTextColumn Header="Tab" Binding="{Binding DocumentTitle}" /> <DataGridTemplateColumn Header="Open"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> diff --git a/src/PacketLogger/Views/MainWindow.axaml b/src/PacketLogger/Views/MainWindow.axaml index 4a86436..e19da9e 100644 --- a/src/PacketLogger/Views/MainWindow.axaml +++ b/src/PacketLogger/Views/MainWindow.axaml @@ -48,7 +48,7 @@ <MenuItem Header="_Packet Sender" Command="{Binding Connect}" Items="{Binding Providers}"> <MenuItem.Styles> <Style Selector="MenuItem"> - <Setter Property="Header" Value="{Binding Name}" /> + <Setter Property="Header" Value="{Binding DocumentTitle}" /> <Setter Property="Command" Value="{Binding OpenSender}" /> <Setter Property="CommandParameter" Value="{Binding SelectedItems, RelativeSource={RelativeSource Self}}" /> -- 2.49.0