mirror of
				https://github.com/2dust/v2rayN.git
				synced 2025-10-26 02:04:40 +00:00 
			
		
		
		
	Compare commits
	
		
			4 commits
		
	
	
		
			84ca5808b9
			...
			ea7ea5a0bf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ea7ea5a0bf | ||
|   | a652fd879b | ||
|   | 326bf334e7 | ||
|   | 21a773f400 | 
					 11 changed files with 255 additions and 83 deletions
				
			
		
							
								
								
									
										2
									
								
								.github/workflows/build-linux.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/build-linux.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -22,7 +22,7 @@ jobs: | |||
|       matrix: | ||||
|         configuration: [Release] | ||||
| 
 | ||||
|     runs-on: ubuntu-22.04 | ||||
|     runs-on: ubuntu-24.04 | ||||
| 
 | ||||
|     steps: | ||||
|     - name: Checkout | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
|     <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> | ||||
|   </PropertyGroup> | ||||
|   <ItemGroup> | ||||
|     <PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" /> | ||||
|     <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" /> | ||||
|     <PackageVersion Include="Avalonia.Desktop" Version="11.3.6" /> | ||||
|     <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" /> | ||||
|  | @ -19,6 +20,7 @@ | |||
|     <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> | ||||
|     <PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" /> | ||||
|     <PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" /> | ||||
|     <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" /> | ||||
|     <PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" /> | ||||
|     <PackageVersion Include="NLog" Version="6.0.4" /> | ||||
|     <PackageVersion Include="sqlite-net-pcl" Version="1.9.172" /> | ||||
|  |  | |||
|  | @ -450,6 +450,14 @@ public class Global | |||
|             "none" | ||||
|     ]; | ||||
| 
 | ||||
|     public static readonly Dictionary<string, string> LogLevelColors = new() | ||||
|     { | ||||
|         { "debug",   "#6C757D" }, | ||||
|         { "info",    "#2ECC71" }, | ||||
|         { "warning", "#FFA500" }, | ||||
|         { "error",   "#E74C3C" }, | ||||
|     }; | ||||
| 
 | ||||
|     public static readonly List<string> InboundTags = | ||||
|     [ | ||||
|         "socks", | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| using System.Collections.Concurrent; | ||||
| using System.Reactive.Linq; | ||||
| using System.Text; | ||||
| using System.Text.RegularExpressions; | ||||
| using ReactiveUI; | ||||
| using ReactiveUI.Fody.Helpers; | ||||
|  | @ -9,9 +10,9 @@ namespace ServiceLib.ViewModels; | |||
| public class MsgViewModel : MyReactiveObject | ||||
| { | ||||
|     private readonly ConcurrentQueue<string> _queueMsg = new(); | ||||
|     private readonly int _numMaxMsg = 500; | ||||
|     private bool _lastMsgFilterNotAvailable; | ||||
|     private bool _blLockShow = false; | ||||
|     private volatile bool _lastMsgFilterNotAvailable; | ||||
|     private int _showLock = 0; // 0 = unlocked, 1 = locked | ||||
|     public int NumMaxMsg { get; } = 500; | ||||
| 
 | ||||
|     [Reactive] | ||||
|     public string MsgFilter { get; set; } | ||||
|  | @ -33,46 +34,52 @@ public class MsgViewModel : MyReactiveObject | |||
|         this.WhenAnyValue( | ||||
|           x => x.AutoRefresh, | ||||
|           y => y == true) | ||||
|               .Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; }); | ||||
|               .Subscribe(c => _config.MsgUIItem.AutoRefresh = AutoRefresh); | ||||
| 
 | ||||
|         AppEvents.SendMsgViewRequested | ||||
|          .AsObservable() | ||||
|          //.ObserveOn(RxApp.MainThreadScheduler) | ||||
|          .Subscribe(async content => await AppendQueueMsg(content)); | ||||
|          .Subscribe(content => _ = AppendQueueMsg(content)); | ||||
|     } | ||||
| 
 | ||||
|     private async Task AppendQueueMsg(string msg) | ||||
|     { | ||||
|         //if (msg == Global.CommandClearMsg) | ||||
|         //{ | ||||
|         //    ClearMsg(); | ||||
|         //    return; | ||||
|         //} | ||||
|         if (AutoRefresh == false) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|         _ = EnqueueQueueMsg(msg); | ||||
| 
 | ||||
|         if (_blLockShow) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|         EnqueueQueueMsg(msg); | ||||
| 
 | ||||
|         if (!_config.UiItem.ShowInTaskbar) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         _blLockShow = true; | ||||
| 
 | ||||
|         await Task.Delay(500); | ||||
|         var txt = string.Join("", _queueMsg.ToArray()); | ||||
|         await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt); | ||||
| 
 | ||||
|         _blLockShow = false; | ||||
|         if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|     private async Task EnqueueQueueMsg(string msg) | ||||
|         try | ||||
|         { | ||||
|             await Task.Delay(500).ConfigureAwait(false); | ||||
| 
 | ||||
|             var sb = new StringBuilder(); | ||||
|             while (_queueMsg.TryDequeue(out var line)) | ||||
|             { | ||||
|                 sb.Append(line); | ||||
|             } | ||||
| 
 | ||||
|             await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString()); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             Interlocked.Exchange(ref _showLock, 0); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void EnqueueQueueMsg(string msg) | ||||
|     { | ||||
|         //filter msg | ||||
|         if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable) | ||||
|  | @ -91,26 +98,17 @@ public class MsgViewModel : MyReactiveObject | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         //Enqueue | ||||
|         if (_queueMsg.Count > _numMaxMsg) | ||||
|         { | ||||
|             for (int k = 0; k < _queueMsg.Count - _numMaxMsg; k++) | ||||
|             { | ||||
|                 _queueMsg.TryDequeue(out _); | ||||
|             } | ||||
|         } | ||||
|         _queueMsg.Enqueue(msg); | ||||
|         if (!msg.EndsWith(Environment.NewLine)) | ||||
|         { | ||||
|             _queueMsg.Enqueue(Environment.NewLine); | ||||
|         } | ||||
|         await Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     public void ClearMsg() | ||||
|     { | ||||
|         _queueMsg.Clear(); | ||||
|     } | ||||
|     //public void ClearMsg() | ||||
|     //{ | ||||
|     //    _queueMsg.Clear(); | ||||
|     //} | ||||
| 
 | ||||
|     private void DoMsgFilter() | ||||
|     { | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ | |||
|     RequestedThemeVariant="Default"> | ||||
|     <Application.Styles> | ||||
|         <semi:SemiTheme /> | ||||
|         <semi:AvaloniaEditSemiTheme /> | ||||
|         <StyleInclude Source="Assets/GlobalStyles.axaml" /> | ||||
|         <StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" /> | ||||
|         <dialogHost:DialogHostStyles /> | ||||
|  |  | |||
							
								
								
									
										129
									
								
								v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								v2rayN/v2rayN.Desktop/Common/TextEditorKeywordHighlighter.cs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | |||
| using Avalonia.Media; | ||||
| using AvaloniaEdit; | ||||
| using AvaloniaEdit.Document; | ||||
| using AvaloniaEdit.Rendering; | ||||
| 
 | ||||
| namespace v2rayN.Desktop.Common; | ||||
| 
 | ||||
| public class KeywordColorizer : DocumentColorizingTransformer | ||||
| { | ||||
|     private readonly string[] _keywords; | ||||
|     private readonly Dictionary<string, IBrush> _brushMap; | ||||
| 
 | ||||
|     public KeywordColorizer(IDictionary<string, IBrush> keywordBrushMap) | ||||
|     { | ||||
|         if (keywordBrushMap == null || keywordBrushMap.Count == 0) | ||||
|         { | ||||
|             throw new ArgumentException("keywordBrushMap must not be null or empty", nameof(keywordBrushMap)); | ||||
|         } | ||||
| 
 | ||||
|         _brushMap = new Dictionary<string, IBrush>(StringComparer.OrdinalIgnoreCase); | ||||
|         foreach (var kvp in keywordBrushMap) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(kvp.Key) || kvp.Value == null) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (!_brushMap.ContainsKey(kvp.Key)) | ||||
|             { | ||||
|                 _brushMap[kvp.Key] = kvp.Value; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (_brushMap.Count == 0) | ||||
|         { | ||||
|             throw new ArgumentException("keywordBrushMap must contain at least one non-empty key with a non-null brush", nameof(keywordBrushMap)); | ||||
|         } | ||||
| 
 | ||||
|         _keywords = _brushMap.Keys.ToArray(); | ||||
|     } | ||||
| 
 | ||||
|     protected override void ColorizeLine(DocumentLine line) | ||||
|     { | ||||
|         var text = CurrentContext.Document.GetText(line); | ||||
|         if (string.IsNullOrEmpty(text)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         foreach (var kw in _keywords) | ||||
|         { | ||||
|             if (string.IsNullOrEmpty(kw)) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             var searchStart = 0; | ||||
|             while (true) | ||||
|             { | ||||
|                 var idx = text.IndexOf(kw, searchStart, StringComparison.OrdinalIgnoreCase); | ||||
|                 if (idx < 0) | ||||
|                 { | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|                 var kwEndIndex = idx + kw.Length; | ||||
|                 if (IsWordCharBefore(text, idx) || IsWordCharAfter(text, kwEndIndex)) | ||||
|                 { | ||||
|                     searchStart = idx + Math.Max(1, kw.Length); | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 var start = line.Offset + idx; | ||||
|                 var end = start + kw.Length; | ||||
| 
 | ||||
|                 if (_brushMap.TryGetValue(kw, out var brush) && brush != null) | ||||
|                 { | ||||
|                     ChangeLinePart(start, end, element => element.TextRunProperties.SetForegroundBrush(brush)); | ||||
|                 } | ||||
| 
 | ||||
|                 searchStart = idx + Math.Max(1, kw.Length); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static bool IsWordCharBefore(string text, int idx) | ||||
|     { | ||||
|         if (idx <= 0) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         var c = text[idx - 1]; | ||||
|         return char.IsLetterOrDigit(c) || c == '_'; | ||||
|     } | ||||
| 
 | ||||
|     private static bool IsWordCharAfter(string text, int idx) | ||||
|     { | ||||
|         if (idx >= text.Length) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         var c = text[idx]; | ||||
|         return char.IsLetterOrDigit(c) || c == '_'; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public static class TextEditorKeywordHighlighter | ||||
| { | ||||
|     public static void Attach(TextEditor editor, IDictionary<string, IBrush> keywordBrushMap) | ||||
|     { | ||||
|         ArgumentNullException.ThrowIfNull(editor); | ||||
| 
 | ||||
|         if (keywordBrushMap == null || keywordBrushMap.Count == 0) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (editor.TextArea?.TextView?.LineTransformers?.OfType<KeywordColorizer>().Any() == true) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var colorizer = new KeywordColorizer(keywordBrushMap); | ||||
|         editor.TextArea.TextView.LineTransformers.Add(colorizer); | ||||
|         editor.TextArea.TextView.InvalidateVisual(); | ||||
|     } | ||||
| } | ||||
|  | @ -5,6 +5,7 @@ using Avalonia.Controls.Notifications; | |||
| using Avalonia.Controls.Primitives; | ||||
| using Avalonia.Media; | ||||
| using Avalonia.Styling; | ||||
| using AvaloniaEdit; | ||||
| using ReactiveUI; | ||||
| using ReactiveUI.Fody.Helpers; | ||||
| using Semi.Avalonia; | ||||
|  | @ -112,7 +113,8 @@ public class ThemeSettingViewModel : MyReactiveObject | |||
|             x.OfType<ContextMenu>(), | ||||
|             x.OfType<DataGridRow>(), | ||||
|             x.OfType<ListBoxItem>(), | ||||
|             x.OfType<HeaderedContentControl>() | ||||
|             x.OfType<HeaderedContentControl>(), | ||||
|             x.OfType<TextEditor>() | ||||
|         )); | ||||
|         style.Add(new Setter() | ||||
|         { | ||||
|  | @ -153,7 +155,8 @@ public class ThemeSettingViewModel : MyReactiveObject | |||
|                 x.OfType<DataGridRow>(), | ||||
|                 x.OfType<ListBoxItem>(), | ||||
|                 x.OfType<HeaderedContentControl>(), | ||||
|                 x.OfType<WindowNotificationManager>() | ||||
|                 x.OfType<WindowNotificationManager>(), | ||||
|                 x.OfType<TextEditor>() | ||||
|             )); | ||||
|             style.Add(new Setter() | ||||
|             { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
|     x:Class="v2rayN.Desktop.Views.MsgView" | ||||
|     xmlns="https://github.com/avaloniaui" | ||||
|     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||||
|     xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" | ||||
|     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | ||||
|     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | ||||
|     xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" | ||||
|  | @ -70,16 +71,17 @@ | |||
|                 Theme="{DynamicResource SimpleToggleSwitch}" /> | ||||
|         </WrapPanel> | ||||
| 
 | ||||
|         <ScrollViewer x:Name="msgScrollViewer" VerticalScrollBarVisibility="Auto"> | ||||
|             <SelectableTextBlock | ||||
|         <avaloniaEdit:TextEditor | ||||
|             Name="txtMsg" | ||||
|             Margin="{StaticResource Margin8}" | ||||
|                 VerticalAlignment="Stretch" | ||||
|                 Classes="TextArea" | ||||
|                 TextAlignment="Left" | ||||
|                 TextWrapping="Wrap"> | ||||
|                 <SelectableTextBlock.ContextMenu> | ||||
|                     <ContextMenu> | ||||
|             IsReadOnly="True" | ||||
|             VerticalScrollBarVisibility="Auto" | ||||
|             WordWrap="True"> | ||||
|             <avaloniaEdit:TextEditor.Options> | ||||
|                 <avaloniaEdit:TextEditorOptions AllowScrollBelowDocument="False"/> | ||||
|             </avaloniaEdit:TextEditor.Options> | ||||
|             <avaloniaEdit:TextEditor.ContextFlyout> | ||||
|                 <MenuFlyout> | ||||
|                     <MenuItem | ||||
|                         x:Name="menuMsgViewSelectAll" | ||||
|                         Click="menuMsgViewSelectAll_Click" | ||||
|  | @ -96,9 +98,9 @@ | |||
|                         x:Name="menuMsgViewClear" | ||||
|                         Click="menuMsgViewClear_Click" | ||||
|                         Header="{x:Static resx:ResUI.menuMsgViewClear}" /> | ||||
|                     </ContextMenu> | ||||
|                 </SelectableTextBlock.ContextMenu> | ||||
|             </SelectableTextBlock> | ||||
|         </ScrollViewer> | ||||
|                 </MenuFlyout> | ||||
|             </avaloniaEdit:TextEditor.ContextFlyout> | ||||
|         </avaloniaEdit:TextEditor> | ||||
| 
 | ||||
|     </DockPanel> | ||||
| </UserControl> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| using System.Reactive.Disposables; | ||||
| using Avalonia.Controls; | ||||
| using Avalonia.Interactivity; | ||||
| using Avalonia.Media; | ||||
| using Avalonia.ReactiveUI; | ||||
| using Avalonia.Threading; | ||||
| using ReactiveUI; | ||||
|  | @ -10,13 +10,11 @@ namespace v2rayN.Desktop.Views; | |||
| 
 | ||||
| public partial class MsgView : ReactiveUserControl<MsgViewModel> | ||||
| { | ||||
|     private readonly ScrollViewer _scrollViewer; | ||||
|     //private const int KeepLines = 30; | ||||
| 
 | ||||
|     public MsgView() | ||||
|     { | ||||
|         InitializeComponent(); | ||||
|         _scrollViewer = this.FindControl<ScrollViewer>("msgScrollViewer"); | ||||
| 
 | ||||
|         ViewModel = new MsgViewModel(UpdateViewHandler); | ||||
| 
 | ||||
|         this.WhenActivated(disposables => | ||||
|  | @ -24,6 +22,11 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel> | |||
|             this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables); | ||||
|             this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables); | ||||
|         }); | ||||
| 
 | ||||
|         TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( | ||||
|                 kv => kv.Key, | ||||
|                 kv => (IBrush)new SolidColorBrush(Color.Parse(kv.Value)) | ||||
|             )); | ||||
|     } | ||||
| 
 | ||||
|     private async Task<bool> UpdateViewHandler(EViewAction action, object? obj) | ||||
|  | @ -34,8 +37,7 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel> | |||
|                 if (obj is null) | ||||
|                     return false; | ||||
| 
 | ||||
|                 Dispatcher.UIThread.Post(() => | ||||
|                     ShowMsg(obj), | ||||
|                 Dispatcher.UIThread.Post(() => ShowMsg(obj), | ||||
|                     DispatcherPriority.ApplicationIdle); | ||||
|                 break; | ||||
|         } | ||||
|  | @ -44,23 +46,41 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel> | |||
| 
 | ||||
|     private void ShowMsg(object msg) | ||||
|     { | ||||
|         txtMsg.Text = msg.ToString(); | ||||
|         txtMsg.BeginChange(); | ||||
| 
 | ||||
|         //var lineCount = txtMsg.LineCount; | ||||
|         //if (lineCount > ViewModel?.NumMaxMsg) | ||||
|         //{ | ||||
|         //    var cutLine = txtMsg.Document.GetLineByNumber(lineCount - KeepLines); | ||||
|         //    txtMsg.Document.Remove(0, cutLine.Offset); | ||||
|         //} | ||||
|         if (txtMsg.LineCount > ViewModel?.NumMaxMsg) | ||||
|         { | ||||
|             ClearMsg(); | ||||
|         } | ||||
| 
 | ||||
|         txtMsg.AppendText(msg.ToString()); | ||||
|         if (togScrollToEnd.IsChecked ?? true) | ||||
|         { | ||||
|             _scrollViewer?.ScrollToEnd(); | ||||
|             txtMsg.ScrollToEnd(); | ||||
|         } | ||||
| 
 | ||||
|         txtMsg.EndChange(); | ||||
|     } | ||||
| 
 | ||||
|     public void ClearMsg() | ||||
|     { | ||||
|         ViewModel?.ClearMsg(); | ||||
|         txtMsg.Text = ""; | ||||
|         txtMsg.Text = string.Empty; | ||||
|         txtMsg.AppendText("----- Message cleared -----\n"); | ||||
|     } | ||||
| 
 | ||||
|     private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e) | ||||
|     { | ||||
|         txtMsg.Focus(); | ||||
|         Dispatcher.UIThread.Post(() => | ||||
|         { | ||||
|             txtMsg.TextArea.Focus(); | ||||
|             txtMsg.SelectAll(); | ||||
|         }, DispatcherPriority.Render); | ||||
|     } | ||||
| 
 | ||||
|     private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e) | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ | |||
| 	</PropertyGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Avalonia.AvaloniaEdit" /> | ||||
| 		<PackageReference Include="Avalonia.Controls.DataGrid"> | ||||
| 			<TreatAsUsed>true</TreatAsUsed> | ||||
| 		</PackageReference> | ||||
|  | @ -17,6 +18,7 @@ | |||
| 		<PackageReference Include="Avalonia.ReactiveUI" /> | ||||
| 		<PackageReference Include="MessageBox.Avalonia" /> | ||||
| 		<PackageReference Include="Semi.Avalonia" /> | ||||
| 		<PackageReference Include="Semi.Avalonia.AvaloniaEdit" /> | ||||
| 		<PackageReference Include="Semi.Avalonia.DataGrid"> | ||||
| 			<TreatAsUsed>true</TreatAsUsed> | ||||
| 		</PackageReference> | ||||
|  |  | |||
|  | @ -48,18 +48,25 @@ public partial class MsgView | |||
|     private void ShowMsg(object msg) | ||||
|     { | ||||
|         txtMsg.BeginChange(); | ||||
|         txtMsg.Text = msg.ToString(); | ||||
| 
 | ||||
|         if (txtMsg.LineCount > ViewModel?.NumMaxMsg) | ||||
|         { | ||||
|             ClearMsg(); | ||||
|         } | ||||
| 
 | ||||
|         txtMsg.AppendText(msg.ToString()); | ||||
|         if (togScrollToEnd.IsChecked ?? true) | ||||
|         { | ||||
|             txtMsg.ScrollToEnd(); | ||||
|         } | ||||
| 
 | ||||
|         txtMsg.EndChange(); | ||||
|     } | ||||
| 
 | ||||
|     public void ClearMsg() | ||||
|     { | ||||
|         ViewModel?.ClearMsg(); | ||||
|         txtMsg.Clear(); | ||||
|         txtMsg.AppendText("----- Message cleared -----\n"); | ||||
|     } | ||||
| 
 | ||||
|     private void menuMsgViewSelectAll_Click(object sender, System.Windows.RoutedEventArgs e) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue