#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::ApplicationModel::DataTransfer; 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::CopyLogButton_Click(IInspectable const&, RoutedEventArgs const&) { try { std::wstring allLogText; // Collect all log messages from the UI collection { std::lock_guard lock(m_uiUpdateMutex); for (uint32_t i = 0; i < m_logCollection.Size(); ++i) { if (auto logString = m_logCollection.GetAt(i).try_as()) { allLogText += logString->c_str(); allLogText += L"\r\n"; // Windows line ending for clipboard } } } if (!allLogText.empty()) { // Remove the last line ending if (allLogText.length() >= 2) { allLogText = allLogText.substr(0, allLogText.length() - 2); } // Copy to clipboard using Windows API if (OpenClipboard(NULL)) { EmptyClipboard(); size_t len = (allLogText.length() + 1) * sizeof(wchar_t); HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len); if (hMem != NULL) { wchar_t* pMem = static_cast(GlobalLock(hMem)); if (pMem != NULL) { wcscpy_s(pMem, allLogText.length() + 1, allLogText.c_str()); GlobalUnlock(hMem); SetClipboardData(CF_UNICODETEXT, hMem); } } CloseClipboard(); } // Logs copied to clipboard - no need to log this action } else { // No log messages to copy - no need to log this } } catch (...) { // Failed to copy logs - no need to log this error } } 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 cleared - no need to log this action } 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"; } } void LogMessagePage::SetLogItemBackground(uint32_t index, LogLevel level) { // This approach doesn't work well with ItemsControl template system. // Instead, we'll modify the approach to use a custom DataTemplate with binding. // For now, we'll skip the background coloring and implement a simpler solution. // TODO: Implement background coloring using a different approach } void LogMessagePage::LogBorder_Loaded(IInspectable const& sender, RoutedEventArgs const&) { try { if (auto border = sender.try_as()) { // Find the TextBlock child to get the log text if (auto textBlock = border.Child().try_as()) { auto logText = std::wstring(textBlock.Text().c_str()); // Check log level in the text and set background color accordingly if (logText.find(L"ERROR:") != std::wstring::npos) { // Dark red background for ERROR logs in dark theme border.Background(Microsoft::UI::Xaml::Media::SolidColorBrush( Microsoft::UI::ColorHelper::FromArgb(255, 80, 30, 30))); // Dark red } else if (logText.find(L"WARN:") != std::wstring::npos) { // Dark yellow background for WARNING logs in dark theme border.Background(Microsoft::UI::Xaml::Media::SolidColorBrush( Microsoft::UI::ColorHelper::FromArgb(255, 80, 70, 30))); // Dark yellow } else { // Dark background for other log levels border.Background(Microsoft::UI::Xaml::Media::SolidColorBrush( Microsoft::UI::ColorHelper::FromArgb(255, 45, 45, 45))); // #2D2D2D } } } } catch (...) { // Ignore errors in background color setting } } }