| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- using System;
- using System.Collections.ObjectModel;
- using System.ComponentModel;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Data;
- using System.Windows.Input;
- using System.Windows.Media;
- using TeamAAS.Dialogs.Dialogs;
- namespace TeamAAS.Logging.Controls
- {
- /// <summary>
- /// 实时日志控件:订阅 <see cref="LogStream.Published"/> 联动显示 AppLogger / PluginLogger 的日志。
- /// - 类别筛选:Debug/Info/Warning/Error/Critical 多选开关;
- /// - 显示判定:公共日志恒显;插件日志由各节点「显示详细度」闸门决定是否进流(与是否落盘无关);
- /// - 插件条目上的 L2/L4 为归一化档位标签,仅用于区分常规/详细,不参与显隐。
- /// </summary>
- public partial class LogView : UserControl
- {
- /// <summary>显示上限依赖属性的默认值(可在 XAML 用 MaxEntries 覆盖)。</summary>
- private const int DefaultMaxEntries = 500;
- /// <summary>显示上限:控件最多保留的最近条目数,超出移除最旧。可在 XAML 用 MaxEntries 设置。</summary>
- public static readonly DependencyProperty MaxEntriesProperty =
- DependencyProperty.Register(nameof(MaxEntries), typeof(int), typeof(LogView),
- new PropertyMetadata(DefaultMaxEntries, OnMaxEntriesChanged));
- /// <summary>显示上限(最近 N 条),默认 500,<=0 视为 1。</summary>
- public int MaxEntries
- {
- get => (int)GetValue(MaxEntriesProperty);
- set => SetValue(MaxEntriesProperty, value);
- }
- private static void OnMaxEntriesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- => (d as LogView)?.TrimToMax();
- private readonly ObservableCollection<LogEntry> _items = new ObservableCollection<LogEntry>();
- private readonly ICollectionView _view;
- private ScrollViewer _scrollViewer;
- private bool _subscribed;
- public LogView()
- {
- InitializeComponent();
- _view = CollectionViewSource.GetDefaultView(_items);
- _view.Filter = o => IsCategoryEnabled(((LogEntry)o).Category);
- LogList.ItemsSource = _view;
- Loaded += OnLoaded;
- Unloaded += OnUnloaded;
- }
- private void OnLoaded(object sender, RoutedEventArgs e)
- {
- if (_subscribed) return;
- // 首次加载回填历史缓冲(新建控件实例时 _items 为空)
- if (_items.Count == 0)
- {
- foreach (var entry in LogStream.Snapshot())
- AppendInternal(entry);
- }
- LogStream.Published += OnPublished;
- _subscribed = true;
- ScrollToEnd();
- }
- private void OnUnloaded(object sender, RoutedEventArgs e)
- {
- if (!_subscribed) return;
- LogStream.Published -= OnPublished;
- _subscribed = false;
- }
- private void OnPublished(LogEntry entry)
- {
- var dispatcher = Application.Current?.Dispatcher;
- if (dispatcher != null && !dispatcher.CheckAccess())
- dispatcher.BeginInvoke(new Action(() => Append(entry)));
- else
- Append(entry);
- }
- private void Append(LogEntry entry)
- {
- bool atBottom = IsScrolledToEnd();
- AppendInternal(entry);
- if (atBottom) ScrollToEnd();
- }
- private void AppendInternal(LogEntry entry)
- {
- _items.Add(entry);
- TrimToMax();
- }
- private void TrimToMax()
- {
- int max = MaxEntries <= 0 ? 1 : MaxEntries;
- while (_items.Count > max)
- _items.RemoveAt(0);
- }
- private void CategoryToggle_Changed(object sender, RoutedEventArgs e)
- {
- // 初始化阶段(ToggleButton 尚未就绪)不刷新
- if (_view != null && LogList != null)
- _view.Refresh();
- }
- private void BtnClear_Click(object sender, RoutedEventArgs e)
- {
- _items.Clear();
- }
- // 双击一条日志 → 弹出详情窗(含来源/时间/类别/完整内容,支持复制)
- private void LogList_MouseDoubleClick(object sender, MouseButtonEventArgs e)
- {
- var entry = FindEntry(e.OriginalSource as DependencyObject) ?? LogList.SelectedItem as LogEntry;
- if (entry == null) return;
- var window = new LogDetailWindow(entry) { Owner = Window.GetWindow(this) };
- window.ShowDialog();
- }
- // 从双击命中点沿(视觉/逻辑)树上溯,找到 DataContext 为 LogEntry 的元素
- private static LogEntry FindEntry(DependencyObject node)
- {
- while (node != null)
- {
- if (node is FrameworkElement fe && fe.DataContext is LogEntry entry)
- return entry;
- node = node is Visual
- ? VisualTreeHelper.GetParent(node)
- : LogicalTreeHelper.GetParent(node);
- }
- return null;
- }
- private bool IsCategoryEnabled(LogLevel category)
- {
- switch (category)
- {
- case LogLevel.Warning: return TgWarning == null || TgWarning.IsChecked == true;
- case LogLevel.Error: return TgError == null || TgError.IsChecked == true;
- default: return TgInfo == null || TgInfo.IsChecked == true; // Info
- }
- }
- private bool IsScrolledToEnd()
- {
- if (_scrollViewer == null) _scrollViewer = FindScrollViewer(LogList);
- if (_scrollViewer == null) return true;
- return _scrollViewer.VerticalOffset >= _scrollViewer.ScrollableHeight - 4;
- }
- private void ScrollToEnd()
- {
- if (LogList.Items.Count > 0)
- LogList.ScrollIntoView(LogList.Items[LogList.Items.Count - 1]);
- }
- private static ScrollViewer FindScrollViewer(DependencyObject root)
- {
- if (root == null) return null;
- int count = VisualTreeHelper.GetChildrenCount(root);
- for (int i = 0; i < count; i++)
- {
- var child = VisualTreeHelper.GetChild(root, i);
- if (child is ScrollViewer sv) return sv;
- var result = FindScrollViewer(child);
- if (result != null) return result;
- }
- return null;
- }
- }
- }
|