#include "pch.h" #include "LogMessagePage.xaml.h" #if __has_include("LogMessagePage.g.cpp") #include "LogMessagePage.g.cpp" #endif using namespace winrt; using namespace winrt::Microsoft::UI::Xaml; using namespace winrt::Microsoft::UI::Xaml::Controls; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace std::chrono; namespace winrt::Vav2Player::implementation { // Static members std::weak_ptr LogMessagePage::s_instance; std::mutex LogMessagePage::s_instanceMutex; LogMessagePage::LogMessagePage() { InitializeComponent(); m_logCollection = winrt::single_threaded_observable_vector(); // Set ItemsSource after InitializeComponent to ensure controls are ready if (LogItemsControl()) { LogItemsControl().ItemsSource(m_logCollection); } // Initialize Observer pattern with LogManager InitializeLogObserver(); } LogMessagePage::~LogMessagePage() { // Remove observer callback when LogMessagePage is destroyed if (m_observerInitialized) { ::Vav2Player::LogManager::GetInstance().RegisterLogAddedCallback(nullptr); } } void LogMessagePage::ClearLogButton_Click(IInspectable const&, RoutedEventArgs const&) { // Clear logs in LogManager (single source of truth) ::Vav2Player::LogManager::GetInstance().ClearLogs(); // Clear UI collection { std::lock_guard lock(m_uiUpdateMutex); m_logCollection.Clear(); LogCountText().Text(L"0 messages"); } // Release mutex before logging to avoid deadlock // Log the clear action (called after releasing mutex to prevent deadlock) ::Vav2Player::LogManager::GetInstance().LogInfo(L"Log cleared by user", L"LogMessagePage"); } void LogMessagePage::AutoScrollCheckBox_CheckedChanged(IInspectable const& sender, RoutedEventArgs const&) { if (auto checkbox = sender.try_as()) { m_autoScroll = checkbox.IsChecked().GetBoolean(); } } void LogMessagePage::LogLevelFilterComboBox_SelectionChanged(IInspectable const& sender, SelectionChangedEventArgs const&) { // Prevent crashes during XAML initialization if (!m_observerInitialized || !m_logCollection) { return; } if (auto comboBox = sender.try_as()) { if (auto selectedItem = comboBox.SelectedItem().try_as()) { auto tag = selectedItem.Tag().try_as(); if (tag == L"DEBUG") m_currentFilter = LogLevel::Debug; else if (tag == L"INFO") m_currentFilter = LogLevel::Info; else if (tag == L"WARNING") m_currentFilter = LogLevel::Warning; else if (tag == L"ERROR") m_currentFilter = LogLevel::Error; else m_currentFilter = LogLevel::Debug; // ALL UpdateLogDisplay(); } } } void LogMessagePage::InitializeLogObserver() { if (m_observerInitialized) { return; } // Register as observer with LogManager using Observer pattern auto& logManager = ::Vav2Player::LogManager::GetInstance(); // Set up callback to receive log messages from LogManager logManager.RegisterLogAddedCallback([this](const ::Vav2Player::LogMessage& message) { OnLogAdded(message); }); // Load existing messages from LogManager const auto& existingMessages = logManager.GetLogMessages(); for (const auto& message : existingMessages) { OnLogAdded(*message); } m_observerInitialized = true; } void LogMessagePage::OnLogAdded(const ::Vav2Player::LogMessage& message) { // Dispatch to UI thread if needed if (!DispatcherQueue().HasThreadAccess()) { DispatcherQueue().TryEnqueue([this, message]() { OnLogAdded(message); }); return; } // Prevent crashes during initialization if (!m_logCollection || !LogCountText()) { return; } std::lock_guard lock(m_uiUpdateMutex); // Update UI if message should be shown according to current filter if (ShouldShowMessage(message.level)) { // Format message for display using LogManager's data std::wstring formattedMessage = FormatLogMessageForUI(message); // Add the formatted message to the UI collection m_logCollection.Append(winrt::box_value(formattedMessage)); // Limit UI collection size (LogManager handles its own size limits) static constexpr uint32_t UI_MAX_MESSAGES = 500; // Smaller than LogManager limit while (m_logCollection.Size() > UI_MAX_MESSAGES) { m_logCollection.RemoveAt(0); } } // Update count from LogManager (safe now that callback is called after mutex release) const auto& allMessages = ::Vav2Player::LogManager::GetInstance().GetLogMessages(); LogCountText().Text(std::to_wstring(allMessages.size()) + L" messages"); // Auto-scroll if enabled if (m_autoScroll) { ScrollToBottom(); } } // Removed AddLogMessage methods - LogMessagePage now observes LogManager instead of managing logs directly std::shared_ptr LogMessagePage::GetInstance() { std::lock_guard lock(s_instanceMutex); return s_instance.lock(); } void LogMessagePage::SetInstance(std::shared_ptr instance) { std::lock_guard lock(s_instanceMutex); s_instance = instance; } void LogMessagePage::UpdateLogDisplay() { // Prevent crashes during initialization if (!m_logCollection || !LogCountText()) { return; } std::lock_guard lock(m_uiUpdateMutex); // Clear and repopulate UI from LogManager (single source of truth) m_logCollection.Clear(); const auto& allMessages = ::Vav2Player::LogManager::GetInstance().GetLogMessages(); for (const auto& logMessage : allMessages) { if (ShouldShowMessage(logMessage->level)) { std::wstring formattedMessage = FormatLogMessageForUI(*logMessage); m_logCollection.Append(winrt::box_value(formattedMessage)); } } // Update count LogCountText().Text(std::to_wstring(allMessages.size()) + L" messages"); if (m_autoScroll) { ScrollToBottom(); } } void LogMessagePage::ScrollToBottom() { // Prevent crashes during initialization if (!m_logCollection || !LogScrollViewer()) { return; } // Schedule scroll to bottom on next UI update DispatcherQueue().TryEnqueue([this]() { if (m_logCollection && m_logCollection.Size() > 0 && LogScrollViewer()) { LogScrollViewer().ChangeView(nullptr, LogScrollViewer().ScrollableHeight(), nullptr); } }); } std::wstring LogMessagePage::FormatLogMessageForUI(const ::Vav2Player::LogMessage& message) const { std::wstring formattedMessage = L"[" + message.timestamp + L"] " + GetLogLevelString(message.level) + L": " + message.message; if (!message.source.empty()) { formattedMessage += L" (" + message.source + L")"; } return formattedMessage; } bool LogMessagePage::ShouldShowMessage(LogLevel level) const { // If filter is "ALL" (Debug), show everything if (m_currentFilter == LogLevel::Debug) return true; // Otherwise, show messages at current filter level or higher return static_cast(level) >= static_cast(m_currentFilter); } std::wstring LogMessagePage::GetLogLevelString(LogLevel level) const { switch (level) { case LogLevel::Debug: return L"DEBUG"; case LogLevel::Info: return L"INFO"; case LogLevel::Warning: return L"WARN"; case LogLevel::Error: return L"ERROR"; default: return L"UNKNOWN"; } } }