Files
video-v1/vav2/platforms/windows/applications/vav2player/Vav2Player/LogMessagePage.xaml.cpp

365 lines
13 KiB
C++

#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> LogMessagePage::s_instance;
std::mutex LogMessagePage::s_instanceMutex;
LogMessagePage::LogMessagePage()
{
InitializeComponent();
m_logCollection = winrt::single_threaded_observable_vector<IInspectable>();
// 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<std::mutex> lock(m_uiUpdateMutex);
for (uint32_t i = 0; i < m_logCollection.Size(); ++i)
{
if (auto logString = m_logCollection.GetAt(i).try_as<winrt::hstring>())
{
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<wchar_t*>(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<std::mutex> 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<CheckBox>())
{
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<ComboBox>())
{
if (auto selectedItem = comboBox.SelectedItem().try_as<ComboBoxItem>())
{
auto tag = selectedItem.Tag().try_as<hstring>();
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<std::mutex> 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> LogMessagePage::GetInstance()
{
std::lock_guard<std::mutex> lock(s_instanceMutex);
return s_instance.lock();
}
void LogMessagePage::SetInstance(std::shared_ptr<LogMessagePage> instance)
{
std::lock_guard<std::mutex> lock(s_instanceMutex);
s_instance = instance;
}
void LogMessagePage::UpdateLogDisplay()
{
// Prevent crashes during initialization
if (!m_logCollection || !LogCountText()) {
return;
}
std::lock_guard<std::mutex> 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<int>(level) >= static_cast<int>(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<Microsoft::UI::Xaml::Controls::Border>())
{
// Find the TextBlock child to get the log text
if (auto textBlock = border.Child().try_as<Microsoft::UI::Xaml::Controls::TextBlock>())
{
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
}
}
}