2026-04-22 02:47:28 +08:00
|
|
|
#include "Platform/Win32/EditorWindowScreenshotController.h"
|
2026-04-05 20:46:24 +08:00
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
#include "Support/ExecutablePath.h"
|
2026-04-05 20:46:24 +08:00
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <system_error>
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
namespace XCEngine::UI::Editor::App {
|
2026-04-05 20:46:24 +08:00
|
|
|
|
2026-04-07 03:51:26 +08:00
|
|
|
namespace {
|
|
|
|
|
|
2026-04-10 00:41:28 +08:00
|
|
|
std::filesystem::path ResolveBuildCaptureRoot(const std::filesystem::path& requestedCaptureRoot) {
|
2026-04-21 00:57:14 +08:00
|
|
|
std::filesystem::path captureRoot = App::GetExecutableDirectory() / "captures";
|
2026-04-10 00:41:28 +08:00
|
|
|
const std::filesystem::path scenarioPath = requestedCaptureRoot.parent_path().filename();
|
|
|
|
|
if (!scenarioPath.empty() && scenarioPath != "captures") {
|
|
|
|
|
captureRoot /= scenarioPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return captureRoot.lexically_normal();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 03:51:26 +08:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
void EditorWindowScreenshotController::Initialize(const std::filesystem::path& captureRoot) {
|
2026-04-10 00:41:28 +08:00
|
|
|
m_captureRoot = ResolveBuildCaptureRoot(captureRoot);
|
2026-04-05 20:46:24 +08:00
|
|
|
m_historyRoot = (m_captureRoot / "history").lexically_normal();
|
|
|
|
|
m_latestCapturePath = (m_captureRoot / "latest.png").lexically_normal();
|
2026-04-22 02:14:26 +08:00
|
|
|
m_activeHistoryCapturePath.clear();
|
2026-04-05 20:46:24 +08:00
|
|
|
m_captureCount = 0;
|
|
|
|
|
m_capturePending = false;
|
|
|
|
|
m_pendingReason.clear();
|
2026-04-10 00:41:28 +08:00
|
|
|
m_lastCaptureSummary = "Output: " + m_captureRoot.string();
|
2026-04-05 20:46:24 +08:00
|
|
|
m_lastCaptureError.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
void EditorWindowScreenshotController::Shutdown() {
|
2026-04-22 02:14:26 +08:00
|
|
|
m_activeHistoryCapturePath.clear();
|
2026-04-05 20:46:24 +08:00
|
|
|
m_capturePending = false;
|
|
|
|
|
m_pendingReason.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
void EditorWindowScreenshotController::RequestCapture(std::string reason) {
|
2026-04-05 20:46:24 +08:00
|
|
|
m_pendingReason = reason.empty() ? "capture" : std::move(reason);
|
|
|
|
|
m_capturePending = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
bool EditorWindowScreenshotController::TryBeginCapture(std::filesystem::path& outHistoryPath) {
|
2026-04-22 02:14:26 +08:00
|
|
|
outHistoryPath.clear();
|
2026-04-07 03:51:26 +08:00
|
|
|
if (!m_capturePending) {
|
2026-04-22 02:14:26 +08:00
|
|
|
return false;
|
2026-04-05 20:46:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::error_code errorCode = {};
|
2026-04-06 04:27:54 +08:00
|
|
|
std::filesystem::create_directories(m_captureRoot, errorCode);
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
m_lastCaptureError = "Failed to create screenshot directory: " + m_captureRoot.string();
|
|
|
|
|
m_lastCaptureSummary = "AutoShot failed";
|
2026-04-22 02:14:26 +08:00
|
|
|
ResetPendingRequest();
|
|
|
|
|
return false;
|
2026-04-06 04:27:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
std::filesystem::create_directories(m_historyRoot, errorCode);
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
m_lastCaptureError = "Failed to create screenshot directory: " + m_historyRoot.string();
|
|
|
|
|
m_lastCaptureSummary = "AutoShot failed";
|
2026-04-22 02:14:26 +08:00
|
|
|
ResetPendingRequest();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_activeHistoryCapturePath = BuildHistoryCapturePath(m_pendingReason);
|
|
|
|
|
outHistoryPath = m_activeHistoryCapturePath;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
void EditorWindowScreenshotController::CompleteCaptureSuccess(
|
|
|
|
|
const std::filesystem::path& historyPath) {
|
2026-04-22 02:14:26 +08:00
|
|
|
const std::filesystem::path resolvedHistoryPath =
|
|
|
|
|
historyPath.empty() ? m_activeHistoryCapturePath : historyPath;
|
|
|
|
|
if (resolvedHistoryPath.empty()) {
|
|
|
|
|
CompleteCaptureFailure("Capture completed without a valid history path.");
|
2026-04-05 20:46:24 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:14:26 +08:00
|
|
|
std::error_code errorCode = {};
|
|
|
|
|
const std::uintmax_t historyFileSize =
|
|
|
|
|
std::filesystem::file_size(resolvedHistoryPath, errorCode);
|
|
|
|
|
if (errorCode || historyFileSize == 0u) {
|
|
|
|
|
CompleteCaptureFailure("Capture completed without a valid PNG payload.");
|
2026-04-05 20:46:24 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 04:27:54 +08:00
|
|
|
errorCode.clear();
|
|
|
|
|
std::filesystem::copy_file(
|
2026-04-22 02:14:26 +08:00
|
|
|
resolvedHistoryPath,
|
2026-04-06 04:27:54 +08:00
|
|
|
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";
|
2026-04-22 02:14:26 +08:00
|
|
|
ResetPendingRequest();
|
2026-04-06 04:27:54 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 20:46:24 +08:00
|
|
|
++m_captureCount;
|
|
|
|
|
m_lastCaptureError.clear();
|
|
|
|
|
m_lastCaptureSummary =
|
2026-04-22 02:14:26 +08:00
|
|
|
"Shot: latest.png | " + resolvedHistoryPath.filename().string();
|
|
|
|
|
ResetPendingRequest();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
void EditorWindowScreenshotController::CompleteCaptureFailure(std::string error) {
|
2026-04-22 02:14:26 +08:00
|
|
|
if (!m_activeHistoryCapturePath.empty()) {
|
|
|
|
|
std::error_code deleteError = {};
|
|
|
|
|
std::filesystem::remove(m_activeHistoryCapturePath, deleteError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_lastCaptureError = error.empty()
|
|
|
|
|
? "Screenshot capture failed."
|
|
|
|
|
: std::move(error);
|
|
|
|
|
m_lastCaptureSummary = "AutoShot failed";
|
|
|
|
|
ResetPendingRequest();
|
2026-04-05 20:46:24 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
bool EditorWindowScreenshotController::HasPendingCapture() const {
|
2026-04-05 20:46:24 +08:00
|
|
|
return m_capturePending;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
const std::filesystem::path& EditorWindowScreenshotController::GetLatestCapturePath() const {
|
2026-04-05 20:46:24 +08:00
|
|
|
return m_latestCapturePath;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
const std::string& EditorWindowScreenshotController::GetLastCaptureSummary() const {
|
2026-04-05 20:46:24 +08:00
|
|
|
return m_lastCaptureSummary;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
const std::string& EditorWindowScreenshotController::GetLastCaptureError() const {
|
2026-04-05 20:46:24 +08:00
|
|
|
return m_lastCaptureError;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
void EditorWindowScreenshotController::ResetPendingRequest() {
|
2026-04-22 02:14:26 +08:00
|
|
|
m_capturePending = false;
|
|
|
|
|
m_pendingReason.clear();
|
|
|
|
|
m_activeHistoryCapturePath.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
std::filesystem::path EditorWindowScreenshotController::BuildHistoryCapturePath(
|
|
|
|
|
std::string_view reason) const {
|
2026-04-05 20:46:24 +08:00
|
|
|
std::ostringstream filename;
|
|
|
|
|
filename << BuildTimestampString()
|
|
|
|
|
<< '_'
|
|
|
|
|
<< (m_captureCount + 1u)
|
|
|
|
|
<< '_'
|
|
|
|
|
<< SanitizeReason(reason)
|
|
|
|
|
<< ".png";
|
|
|
|
|
return (m_historyRoot / filename.str()).lexically_normal();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
std::string EditorWindowScreenshotController::BuildTimestampString() {
|
2026-04-05 20:46:24 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
std::string EditorWindowScreenshotController::SanitizeReason(std::string_view reason) {
|
2026-04-05 20:46:24 +08:00
|
|
|
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<char>(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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 02:47:28 +08:00
|
|
|
} // namespace XCEngine::UI::Editor::App
|