#include "AutoScreenshot.h" #include "NativeRenderer.h" #include #include #include #include #include #include namespace XCEngine::UI::Editor::Host { namespace { bool IsAutoCaptureOnStartupEnabled() { const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP"); if (value == nullptr || value[0] == '\0') { return false; } std::string normalized = value; for (char& character : normalized) { character = static_cast(std::tolower(static_cast(character))); } return normalized != "0" && normalized != "false" && normalized != "off" && normalized != "no"; } } // namespace void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) { m_captureRoot = captureRoot.lexically_normal(); m_historyRoot = (m_captureRoot / "history").lexically_normal(); m_latestCapturePath = (m_captureRoot / "latest.png").lexically_normal(); m_captureCount = 0; m_capturePending = false; m_pendingReason.clear(); m_lastCaptureSummary.clear(); m_lastCaptureError.clear(); if (IsAutoCaptureOnStartupEnabled()) { RequestCapture("startup"); } } void AutoScreenshotController::Shutdown() { m_capturePending = false; m_pendingReason.clear(); } void AutoScreenshotController::RequestCapture(std::string reason) { m_pendingReason = reason.empty() ? "capture" : std::move(reason); m_capturePending = true; } void AutoScreenshotController::CaptureIfRequested( NativeRenderer& renderer, const ::XCEngine::UI::UIDrawData& drawData, unsigned int width, unsigned int height, bool framePresented) { if (!m_capturePending) { return; } if (!framePresented || drawData.Empty() || width == 0u || height == 0u) { return; } std::error_code errorCode = {}; std::filesystem::create_directories(m_captureRoot, errorCode); if (errorCode) { m_lastCaptureError = "Failed to create screenshot directory: " + m_captureRoot.string(); m_lastCaptureSummary = "AutoShot failed"; m_capturePending = false; return; } std::filesystem::create_directories(m_historyRoot, errorCode); if (errorCode) { m_lastCaptureError = "Failed to create screenshot directory: " + m_historyRoot.string(); m_lastCaptureSummary = "AutoShot failed"; m_capturePending = false; return; } std::string captureError = {}; const std::filesystem::path historyPath = BuildHistoryCapturePath(m_pendingReason); if (!renderer.CaptureToPng(drawData, width, height, historyPath, captureError)) { m_lastCaptureError = std::move(captureError); m_lastCaptureSummary = "AutoShot failed"; m_capturePending = false; return; } errorCode.clear(); std::filesystem::copy_file( historyPath, m_latestCapturePath, std::filesystem::copy_options::overwrite_existing, errorCode); if (errorCode) { m_lastCaptureError = "Failed to update latest screenshot: " + m_latestCapturePath.string(); m_lastCaptureSummary = "AutoShot failed"; m_capturePending = false; return; } ++m_captureCount; m_lastCaptureError.clear(); m_lastCaptureSummary = "Shot: latest.png | " + historyPath.filename().string(); m_capturePending = false; m_pendingReason.clear(); } bool AutoScreenshotController::HasPendingCapture() const { return m_capturePending; } const std::filesystem::path& AutoScreenshotController::GetLatestCapturePath() const { return m_latestCapturePath; } const std::string& AutoScreenshotController::GetLastCaptureSummary() const { return m_lastCaptureSummary; } const std::string& AutoScreenshotController::GetLastCaptureError() const { return m_lastCaptureError; } std::filesystem::path AutoScreenshotController::BuildHistoryCapturePath(std::string_view reason) const { std::ostringstream filename; filename << BuildTimestampString() << '_' << (m_captureCount + 1u) << '_' << SanitizeReason(reason) << ".png"; return (m_historyRoot / filename.str()).lexically_normal(); } std::string AutoScreenshotController::BuildTimestampString() { const auto now = std::chrono::system_clock::now(); const std::time_t currentTime = std::chrono::system_clock::to_time_t(now); std::tm localTime = {}; localtime_s(&localTime, ¤tTime); char buffer[32] = {}; std::snprintf( buffer, sizeof(buffer), "%04d%02d%02d_%02d%02d%02d", localTime.tm_year + 1900, localTime.tm_mon + 1, localTime.tm_mday, localTime.tm_hour, localTime.tm_min, localTime.tm_sec); return buffer; } std::string AutoScreenshotController::SanitizeReason(std::string_view reason) { std::string sanitized = {}; sanitized.reserve(reason.size()); bool lastWasSeparator = false; for (const unsigned char value : reason) { if (std::isalnum(value)) { sanitized.push_back(static_cast(std::tolower(value))); lastWasSeparator = false; continue; } if (!lastWasSeparator) { sanitized.push_back('_'); lastWasSeparator = true; } } while (!sanitized.empty() && sanitized.front() == '_') { sanitized.erase(sanitized.begin()); } while (!sanitized.empty() && sanitized.back() == '_') { sanitized.pop_back(); } return sanitized.empty() ? "capture" : sanitized; } } // namespace XCEngine::UI::Editor::Host