254 lines
8.5 KiB
C++
254 lines
8.5 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::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::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 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<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";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|