feat(xcui): add editor layout persistence validation
This commit is contained in:
@@ -6,57 +6,6 @@ namespace XCEngine::NewEditor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool AreWorkspaceNodesEquivalent(
|
||||
const UIEditorWorkspaceNode& lhs,
|
||||
const UIEditorWorkspaceNode& rhs) {
|
||||
if (lhs.kind != rhs.kind ||
|
||||
lhs.nodeId != rhs.nodeId ||
|
||||
lhs.splitAxis != rhs.splitAxis ||
|
||||
lhs.splitRatio != rhs.splitRatio ||
|
||||
lhs.selectedTabIndex != rhs.selectedTabIndex ||
|
||||
lhs.panel.panelId != rhs.panel.panelId ||
|
||||
lhs.panel.title != rhs.panel.title ||
|
||||
lhs.panel.placeholder != rhs.panel.placeholder ||
|
||||
lhs.children.size() != rhs.children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.children.size(); ++index) {
|
||||
if (!AreWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreWorkspaceModelsEquivalent(
|
||||
const UIEditorWorkspaceModel& lhs,
|
||||
const UIEditorWorkspaceModel& rhs) {
|
||||
return lhs.activePanelId == rhs.activePanelId &&
|
||||
AreWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
||||
}
|
||||
|
||||
bool AreWorkspaceSessionsEquivalent(
|
||||
const UIEditorWorkspaceSession& lhs,
|
||||
const UIEditorWorkspaceSession& rhs) {
|
||||
if (lhs.panelStates.size() != rhs.panelStates.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.panelStates.size(); ++index) {
|
||||
const UIEditorPanelSessionState& lhsState = lhs.panelStates[index];
|
||||
const UIEditorPanelSessionState& rhsState = rhs.panelStates[index];
|
||||
if (lhsState.panelId != rhsState.panelId ||
|
||||
lhsState.open != rhsState.open ||
|
||||
lhsState.visible != rhsState.visible) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
@@ -106,6 +55,20 @@ std::string_view GetUIEditorWorkspaceCommandStatusName(UIEditorWorkspaceCommandS
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::string_view GetUIEditorWorkspaceLayoutOperationStatusName(
|
||||
UIEditorWorkspaceLayoutOperationStatus status) {
|
||||
switch (status) {
|
||||
case UIEditorWorkspaceLayoutOperationStatus::Changed:
|
||||
return "Changed";
|
||||
case UIEditorWorkspaceLayoutOperationStatus::NoOp:
|
||||
return "NoOp";
|
||||
case UIEditorWorkspaceLayoutOperationStatus::Rejected:
|
||||
return "Rejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController::UIEditorWorkspaceController(
|
||||
UIEditorPanelRegistry panelRegistry,
|
||||
UIEditorWorkspaceModel workspace,
|
||||
@@ -148,6 +111,10 @@ UIEditorWorkspaceControllerValidationResult UIEditorWorkspaceController::Validat
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot UIEditorWorkspaceController::CaptureLayoutSnapshot() const {
|
||||
return BuildUIEditorWorkspaceLayoutSnapshot(m_workspace, m_session);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::BuildResult(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
UIEditorWorkspaceCommandStatus status,
|
||||
@@ -162,6 +129,17 @@ UIEditorWorkspaceCommandResult UIEditorWorkspaceController::BuildResult(
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus status,
|
||||
std::string message) const {
|
||||
UIEditorWorkspaceLayoutOperationResult result = {};
|
||||
result.status = status;
|
||||
result.message = std::move(message);
|
||||
result.activePanelId = m_workspace.activePanelId;
|
||||
result.visiblePanelIds = CollectVisiblePanelIds(m_workspace, m_session);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::FinalizeMutation(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
bool changed,
|
||||
@@ -197,6 +175,71 @@ const UIEditorPanelDescriptor* UIEditorWorkspaceController::FindPanelDescriptor(
|
||||
return FindUIEditorPanelDescriptor(m_panelRegistry, panelId);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(m_panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Panel registry invalid: " + registryValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(snapshot.workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Layout workspace invalid: " + workspaceValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(m_panelRegistry, snapshot.workspace, snapshot.session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Layout session invalid: " + sessionValidation.message);
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, snapshot.workspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(m_session, snapshot.session)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Current state already matches the requested layout snapshot.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
const UIEditorWorkspaceSession previousSession = m_session;
|
||||
m_workspace = snapshot.workspace;
|
||||
m_session = snapshot.session;
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
m_session = previousSession;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Restored layout produced invalid controller state: " + validation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Layout restored.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSerializedLayout(
|
||||
std::string_view serializedLayout) {
|
||||
const UIEditorWorkspaceLayoutLoadResult loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(m_panelRegistry, serializedLayout);
|
||||
if (!loadResult.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Serialized layout rejected: " + loadResult.message);
|
||||
}
|
||||
|
||||
return RestoreLayoutSnapshot(loadResult.snapshot);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
const UIEditorWorkspaceCommand& command) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
@@ -326,8 +369,8 @@ UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ResetWorkspace:
|
||||
if (AreWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) &&
|
||||
AreWorkspaceSessionsEquivalent(m_session, m_baselineSession)) {
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(m_session, m_baselineSession)) {
|
||||
return BuildResult(command, UIEditorWorkspaceCommandStatus::NoOp, "Workspace already matches the baseline state.");
|
||||
}
|
||||
|
||||
|
||||
497
new_editor/src/editor/UIEditorWorkspaceLayoutPersistence.cpp
Normal file
497
new_editor/src/editor/UIEditorWorkspaceLayoutPersistence.cpp
Normal file
@@ -0,0 +1,497 @@
|
||||
#include <XCNewEditor/Editor/UIEditorWorkspaceLayoutPersistence.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::NewEditor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kLayoutHeader = "XCUI_EDITOR_WORKSPACE_LAYOUT";
|
||||
constexpr int kLayoutVersion = 1;
|
||||
|
||||
struct LayoutLine {
|
||||
std::size_t number = 0u;
|
||||
std::string text = {};
|
||||
};
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode code,
|
||||
std::string message) {
|
||||
UIEditorWorkspaceLayoutLoadResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HasTrailingTokens(std::istringstream& stream) {
|
||||
stream >> std::ws;
|
||||
return !stream.eof();
|
||||
}
|
||||
|
||||
std::string MakeLinePrefix(const LayoutLine& line) {
|
||||
return "Line " + std::to_string(line.number) + ": ";
|
||||
}
|
||||
|
||||
bool ParseBinaryFlag(
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
int& outValue,
|
||||
UIEditorWorkspaceLayoutLoadResult& outError,
|
||||
UIEditorWorkspaceLayoutLoadCode code,
|
||||
std::string_view fieldName) {
|
||||
if (!(stream >> outValue) || (outValue != 0 && outValue != 1)) {
|
||||
outError = MakeLoadError(
|
||||
code,
|
||||
MakeLinePrefix(line) + std::string(fieldName) + " must be encoded as 0 or 1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseNodeLine(
|
||||
const LayoutLine& line,
|
||||
std::string& outTag,
|
||||
std::istringstream& outStream) {
|
||||
outStream = std::istringstream(line.text);
|
||||
return static_cast<bool>(outStream >> outTag);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
UIEditorWorkspaceNode& outNode);
|
||||
|
||||
std::string SerializeAxis(UIEditorWorkspaceSplitAxis axis) {
|
||||
return axis == UIEditorWorkspaceSplitAxis::Vertical ? "vertical" : "horizontal";
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseAxis(
|
||||
std::string_view text,
|
||||
UIEditorWorkspaceSplitAxis& outAxis,
|
||||
const LayoutLine& line) {
|
||||
if (text == "horizontal") {
|
||||
outAxis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (text == "vertical") {
|
||||
outAxis = UIEditorWorkspaceSplitAxis::Vertical;
|
||||
return {};
|
||||
}
|
||||
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "split axis must be \"horizontal\" or \"vertical\".");
|
||||
}
|
||||
|
||||
void SerializeNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::ostringstream& stream) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel:
|
||||
stream << "node_panel "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< std::quoted(node.panel.panelId) << ' '
|
||||
<< std::quoted(node.panel.title) << ' '
|
||||
<< (node.panel.placeholder ? 1 : 0) << '\n';
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
stream << "node_tabstack "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< node.selectedTabIndex << ' '
|
||||
<< node.children.size() << '\n';
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
SerializeNodeRecursive(child, stream);
|
||||
}
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
stream << "node_split "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< std::quoted(SerializeAxis(node.splitAxis)) << ' '
|
||||
<< std::setprecision(std::numeric_limits<float>::max_digits10)
|
||||
<< node.splitRatio << '\n';
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
SerializeNodeRecursive(child, stream);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LayoutLine> CollectNonEmptyLines(std::string_view serializedLayout) {
|
||||
std::vector<LayoutLine> lines = {};
|
||||
std::istringstream stream{ std::string(serializedLayout) };
|
||||
std::string text = {};
|
||||
std::size_t lineNumber = 0u;
|
||||
while (std::getline(stream, text)) {
|
||||
++lineNumber;
|
||||
if (!text.empty() && text.back() == '\r') {
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
const std::size_t first = text.find_first_not_of(" \t");
|
||||
if (first == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push_back({ lineNumber, text });
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseHeader(const LayoutLine& line) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string header = {};
|
||||
int version = 0;
|
||||
if (!(stream >> header >> version) || HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidHeader,
|
||||
MakeLinePrefix(line) + "invalid layout header.");
|
||||
}
|
||||
|
||||
if (header != kLayoutHeader) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidHeader,
|
||||
MakeLinePrefix(line) + "layout header magic is invalid.");
|
||||
}
|
||||
|
||||
if (version != kLayoutVersion) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::UnsupportedVersion,
|
||||
MakeLinePrefix(line) + "unsupported layout version " + std::to_string(version) + ".");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseActiveRecord(
|
||||
const LayoutLine& line,
|
||||
std::string& outActivePanelId) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string tag = {};
|
||||
if (!(stream >> tag) || tag != "active" ||
|
||||
!(stream >> std::quoted(outActivePanelId)) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::MissingActiveRecord,
|
||||
MakeLinePrefix(line) + "active record must be `active \"panel-id\"`.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParsePanelNode(
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::string panelId = {};
|
||||
std::string title = {};
|
||||
int placeholderValue = 0;
|
||||
if (!(stream >> std::quoted(nodeId) >> std::quoted(panelId) >> std::quoted(title))) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "panel node record is malformed.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult boolParse = {};
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
placeholderValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
"placeholder")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "panel node record contains trailing tokens.");
|
||||
}
|
||||
|
||||
outNode = BuildUIEditorWorkspacePanel(
|
||||
std::move(nodeId),
|
||||
std::move(panelId),
|
||||
std::move(title),
|
||||
placeholderValue != 0);
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseTabStackNode(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::size_t selectedTabIndex = 0u;
|
||||
std::size_t childCount = 0u;
|
||||
if (!(stream >> std::quoted(nodeId) >> selectedTabIndex >> childCount) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "tab stack node record is malformed.");
|
||||
}
|
||||
|
||||
outNode = {};
|
||||
outNode.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
outNode.nodeId = std::move(nodeId);
|
||||
outNode.selectedTabIndex = selectedTabIndex;
|
||||
outNode.children.reserve(childCount);
|
||||
for (std::size_t childIndex = 0; childIndex < childCount; ++childIndex) {
|
||||
UIEditorWorkspaceNode child = {};
|
||||
UIEditorWorkspaceLayoutLoadResult childResult = ParseNodeRecursive(lines, index, child);
|
||||
if (!childResult.IsValid()) {
|
||||
return childResult;
|
||||
}
|
||||
outNode.children.push_back(std::move(child));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseSplitNode(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::string axisText = {};
|
||||
float splitRatio = 0.0f;
|
||||
if (!(stream >> std::quoted(nodeId) >> std::quoted(axisText) >> splitRatio) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "split node record is malformed.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSplitAxis axis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
if (UIEditorWorkspaceLayoutLoadResult axisResult = ParseAxis(axisText, axis, line);
|
||||
!axisResult.IsValid()) {
|
||||
return axisResult;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult primaryResult = ParseNodeRecursive(lines, index, primary);
|
||||
!primaryResult.IsValid()) {
|
||||
return primaryResult;
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceLayoutLoadResult secondaryResult = ParseNodeRecursive(lines, index, secondary);
|
||||
!secondaryResult.IsValid()) {
|
||||
return secondaryResult;
|
||||
}
|
||||
|
||||
outNode = BuildUIEditorWorkspaceSplit(
|
||||
std::move(nodeId),
|
||||
axis,
|
||||
splitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
if (index >= lines.size()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::UnexpectedEndOfInput,
|
||||
"Unexpected end of input while parsing workspace nodes.");
|
||||
}
|
||||
|
||||
const LayoutLine& line = lines[index++];
|
||||
std::istringstream stream = {};
|
||||
std::string tag = {};
|
||||
if (!ParseNodeLine(line, tag, stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "workspace node record is empty.");
|
||||
}
|
||||
|
||||
if (tag == "node_panel") {
|
||||
return ParsePanelNode(stream, line, outNode);
|
||||
}
|
||||
|
||||
if (tag == "node_tabstack") {
|
||||
return ParseTabStackNode(lines, index, stream, line, outNode);
|
||||
}
|
||||
|
||||
if (tag == "node_split") {
|
||||
return ParseSplitNode(lines, index, stream, line, outNode);
|
||||
}
|
||||
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "unknown workspace node tag '" + tag + "'.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseSessionRecord(
|
||||
const LayoutLine& line,
|
||||
UIEditorPanelSessionState& outState) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string tag = {};
|
||||
int openValue = 0;
|
||||
int visibleValue = 0;
|
||||
if (!(stream >> tag) || tag != "session" ||
|
||||
!(stream >> std::quoted(outState.panelId))) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
MakeLinePrefix(line) + "session record must start with `session`.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult boolParse = {};
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
openValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
"open")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
visibleValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
"visible")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
MakeLinePrefix(line) + "session record contains trailing tokens.");
|
||||
}
|
||||
|
||||
outState.open = openValue != 0;
|
||||
outState.visible = visibleValue != 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot BuildUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
UIEditorWorkspaceLayoutSnapshot snapshot = {};
|
||||
snapshot.workspace = workspace;
|
||||
snapshot.session = session;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceLayoutSnapshotsEquivalent(
|
||||
const UIEditorWorkspaceLayoutSnapshot& lhs,
|
||||
const UIEditorWorkspaceLayoutSnapshot& rhs) {
|
||||
return AreUIEditorWorkspaceModelsEquivalent(lhs.workspace, rhs.workspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(lhs.session, rhs.session);
|
||||
}
|
||||
|
||||
std::string SerializeUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot) {
|
||||
std::ostringstream stream = {};
|
||||
stream << kLayoutHeader << ' ' << kLayoutVersion << '\n';
|
||||
stream << "active " << std::quoted(snapshot.workspace.activePanelId) << '\n';
|
||||
SerializeNodeRecursive(snapshot.workspace.root, stream);
|
||||
for (const UIEditorPanelSessionState& state : snapshot.session.panelStates) {
|
||||
stream << "session "
|
||||
<< std::quoted(state.panelId) << ' '
|
||||
<< (state.open ? 1 : 0) << ' '
|
||||
<< (state.visible ? 1 : 0) << '\n';
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult DeserializeUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
std::string_view serializedLayout) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidPanelRegistry,
|
||||
"Panel registry invalid: " + registryValidation.message);
|
||||
}
|
||||
|
||||
const std::vector<LayoutLine> lines = CollectNonEmptyLines(serializedLayout);
|
||||
if (lines.empty()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::EmptyInput,
|
||||
"Serialized layout input is empty.");
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceLayoutLoadResult headerResult = ParseHeader(lines.front());
|
||||
!headerResult.IsValid()) {
|
||||
return headerResult;
|
||||
}
|
||||
|
||||
if (lines.size() < 2u) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::MissingActiveRecord,
|
||||
"Serialized layout is missing the active panel record.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot snapshot = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult activeResult =
|
||||
ParseActiveRecord(lines[1], snapshot.workspace.activePanelId);
|
||||
!activeResult.IsValid()) {
|
||||
return activeResult;
|
||||
}
|
||||
|
||||
std::size_t index = 2u;
|
||||
if (UIEditorWorkspaceLayoutLoadResult rootResult =
|
||||
ParseNodeRecursive(lines, index, snapshot.workspace.root);
|
||||
!rootResult.IsValid()) {
|
||||
return rootResult;
|
||||
}
|
||||
|
||||
snapshot.session.panelStates.clear();
|
||||
while (index < lines.size()) {
|
||||
UIEditorPanelSessionState state = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult stateResult =
|
||||
ParseSessionRecord(lines[index], state);
|
||||
!stateResult.IsValid()) {
|
||||
return stateResult;
|
||||
}
|
||||
snapshot.session.panelStates.push_back(std::move(state));
|
||||
++index;
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(snapshot.workspace);
|
||||
!workspaceValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidWorkspace,
|
||||
workspaceValidation.message);
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(panelRegistry, snapshot.workspace, snapshot.session);
|
||||
!sessionValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession,
|
||||
sessionValidation.message);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult result = {};
|
||||
result.snapshot = std::move(snapshot);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
@@ -205,6 +205,37 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive(
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AreUIEditorWorkspaceNodesEquivalent(
|
||||
const UIEditorWorkspaceNode& lhs,
|
||||
const UIEditorWorkspaceNode& rhs) {
|
||||
if (lhs.kind != rhs.kind ||
|
||||
lhs.nodeId != rhs.nodeId ||
|
||||
lhs.splitAxis != rhs.splitAxis ||
|
||||
lhs.splitRatio != rhs.splitRatio ||
|
||||
lhs.selectedTabIndex != rhs.selectedTabIndex ||
|
||||
lhs.panel.panelId != rhs.panel.panelId ||
|
||||
lhs.panel.title != rhs.panel.title ||
|
||||
lhs.panel.placeholder != rhs.panel.placeholder ||
|
||||
lhs.children.size() != rhs.children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.children.size(); ++index) {
|
||||
if (!AreUIEditorWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceModelsEquivalent(
|
||||
const UIEditorWorkspaceModel& lhs,
|
||||
const UIEditorWorkspaceModel& rhs) {
|
||||
return lhs.activePanelId == rhs.activePanelId &&
|
||||
AreUIEditorWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() {
|
||||
const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry();
|
||||
const UIEditorPanelDescriptor& rootPanel =
|
||||
|
||||
@@ -198,57 +198,6 @@ void NormalizeWorkspaceSession(
|
||||
TryActivateUIEditorWorkspacePanel(workspace, targetActivePanelId);
|
||||
}
|
||||
|
||||
bool AreWorkspaceNodesEquivalent(
|
||||
const UIEditorWorkspaceNode& lhs,
|
||||
const UIEditorWorkspaceNode& rhs) {
|
||||
if (lhs.kind != rhs.kind ||
|
||||
lhs.nodeId != rhs.nodeId ||
|
||||
lhs.splitAxis != rhs.splitAxis ||
|
||||
lhs.splitRatio != rhs.splitRatio ||
|
||||
lhs.selectedTabIndex != rhs.selectedTabIndex ||
|
||||
lhs.panel.panelId != rhs.panel.panelId ||
|
||||
lhs.panel.title != rhs.panel.title ||
|
||||
lhs.panel.placeholder != rhs.panel.placeholder ||
|
||||
lhs.children.size() != rhs.children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.children.size(); ++index) {
|
||||
if (!AreWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AreWorkspaceModelsEquivalent(
|
||||
const UIEditorWorkspaceModel& lhs,
|
||||
const UIEditorWorkspaceModel& rhs) {
|
||||
return lhs.activePanelId == rhs.activePanelId &&
|
||||
AreWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
||||
}
|
||||
|
||||
bool AreWorkspaceSessionsEquivalent(
|
||||
const UIEditorWorkspaceSession& lhs,
|
||||
const UIEditorWorkspaceSession& rhs) {
|
||||
if (lhs.panelStates.size() != rhs.panelStates.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.panelStates.size(); ++index) {
|
||||
const UIEditorPanelSessionState& lhsState = lhs.panelStates[index];
|
||||
const UIEditorPanelSessionState& rhsState = rhs.panelStates[index];
|
||||
if (lhsState.panelId != rhsState.panelId ||
|
||||
lhsState.open != rhsState.open ||
|
||||
lhsState.visible != rhsState.visible) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceSession BuildDefaultUIEditorWorkspaceSession(
|
||||
@@ -348,6 +297,26 @@ UIEditorWorkspaceSessionValidationResult ValidateUIEditorWorkspaceSession(
|
||||
return {};
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceSessionsEquivalent(
|
||||
const UIEditorWorkspaceSession& lhs,
|
||||
const UIEditorWorkspaceSession& rhs) {
|
||||
if (lhs.panelStates.size() != rhs.panelStates.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.panelStates.size(); ++index) {
|
||||
const UIEditorPanelSessionState& lhsState = lhs.panelStates[index];
|
||||
const UIEditorPanelSessionState& rhsState = rhs.panelStates[index];
|
||||
if (lhsState.panelId != rhsState.panelId ||
|
||||
lhsState.open != rhsState.open ||
|
||||
lhsState.visible != rhsState.visible) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
@@ -391,8 +360,8 @@ bool TryOpenUIEditorWorkspacePanel(
|
||||
state->open = true;
|
||||
state->visible = true;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryCloseUIEditorWorkspacePanel(
|
||||
@@ -412,8 +381,8 @@ bool TryCloseUIEditorWorkspacePanel(
|
||||
state->open = false;
|
||||
state->visible = false;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, {});
|
||||
return !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryShowUIEditorWorkspacePanel(
|
||||
@@ -432,8 +401,8 @@ bool TryShowUIEditorWorkspacePanel(
|
||||
|
||||
state->visible = true;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryHideUIEditorWorkspacePanel(
|
||||
@@ -452,8 +421,8 @@ bool TryHideUIEditorWorkspacePanel(
|
||||
|
||||
state->visible = false;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, {});
|
||||
return !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryActivateUIEditorWorkspacePanel(
|
||||
@@ -469,8 +438,8 @@ bool TryActivateUIEditorWorkspacePanel(
|
||||
}
|
||||
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::NewEditor
|
||||
|
||||
Reference in New Issue
Block a user