M src/PacketLogger/Models/Packets/ClientPacketProvider.cs => src/PacketLogger/Models/Packets/ClientPacketProvider.cs +5 -0
@@ 54,6 54,11 @@ public abstract class ClientPacketProvider : ReactiveObject, IPacketProvider
? _process.BrowserManager.PlayerManager.Get().Player.Name
: null) ?? $"Not in game ({_process.Process.Id})";
+ /// <summary>
+ /// Gets or sets title of document.
+ /// </summary>
+ public string DocumentTitle { get; set; } = string.Empty;
+
/// <inheritdoc />
public abstract bool IsOpen { get; }
M src/PacketLogger/Models/Packets/DummyPacketProvider.cs => src/PacketLogger/Models/Packets/DummyPacketProvider.cs +5 -0
@@ 38,6 38,11 @@ public class DummyPacketProvider : IPacketProvider, IDisposable
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
+ /// <summary>
+ /// Gets or sets title of document.
+ /// </summary>
+ public string DocumentTitle { get; set; } = string.Empty;
+
/// <inheritdoc />
public bool LogReceived
{
M src/PacketLogger/Models/Packets/FilePacketProvider.cs => src/PacketLogger/Models/Packets/FilePacketProvider.cs +5 -0
@@ 39,6 39,11 @@ public class FilePacketProvider : IPacketProvider
_fileName = fileName;
}
+ /// <summary>
+ /// Gets or sets title of document.
+ /// </summary>
+ public string DocumentTitle { get; set; } = string.Empty;
+
/// <inheritdoc />
public string Name => Path.GetFileName(_fileName);
M src/PacketLogger/Models/Packets/IPacketProvider.cs => src/PacketLogger/Models/Packets/IPacketProvider.cs +6 -0
@@ 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;
@@ 27,6 28,11 @@ public interface IPacketProvider : INotifyPropertyChanged, IDisposable
public string Name { get; }
/// <summary>
+ /// Gets title of document, if any.
+ /// </summary>
+ public string DocumentTitle { get; set; }
+
+ /// <summary>
/// Gets whether <see cref="Open"/> was called and successfully finished.
/// </summary>
public bool IsOpen { get; }
A src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs => src/PacketLogger/Models/Titles/NumberedTitleGenerator.cs +180 -0
@@ 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;
+
+/// <summary>
+/// A generator and manager of document titles.
+/// </summary>
+public class NumberedTitleGenerator
+{
+ private readonly SemaphoreSlim _semaphore;
+ private readonly ConcurrentDictionary<string, List<Title>> _titles;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NumberedTitleGenerator"/> class.
+ /// </summary>
+ public NumberedTitleGenerator()
+ {
+ _semaphore = new SemaphoreSlim(1, 1);
+ _titles = new ConcurrentDictionary<string, List<Title>>();
+ }
+
+ /// <summary>
+ /// Add title with given information.
+ /// </summary>
+ /// <param name="setDocumentTitle">The function used for setting new document title.</param>
+ /// <param name="titleChanged">The observable observing changes to the default, unnumbered title.</param>
+ /// <param name="initialTitle">The current initial title.</param>
+ /// <returns>The handle, title will be removed upon disposal.</returns>
+ public TitleHandle AddTitle
+ (
+ Action<string> setDocumentTitle,
+ IObservable<string> 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<Title>(),
+ (_, 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
M src/PacketLogger/ViewModels/DockFactory.cs => src/PacketLogger/ViewModels/DockFactory.cs +9 -3
@@ 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;
M src/PacketLogger/ViewModels/DocumentViewModel.cs => src/PacketLogger/ViewModels/DocumentViewModel.cs +67 -30
@@ 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();
M src/PacketLogger/Views/DocumentView.axaml => src/PacketLogger/Views/DocumentView.axaml +2 -2
@@ 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>
M src/PacketLogger/Views/MainWindow.axaml => src/PacketLogger/Views/MainWindow.axaml +1 -1
@@ 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}}" />