365 lines
13 KiB
C++
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
|
|
}
|
|
}
|
|
} |