447 lines
16 KiB
C++
447 lines
16 KiB
C++
#include <XCEditor/Workspace/UIEditorWindowWorkspaceController.h>
|
|
|
|
#include <XCEditor/Workspace/UIEditorWorkspaceTransfer.h>
|
|
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
namespace XCEngine::UI::Editor {
|
|
|
|
namespace {
|
|
|
|
bool IsSinglePanelRootWindow(
|
|
const UIEditorWindowWorkspaceState& state,
|
|
std::string_view panelId) {
|
|
const UIEditorWorkspaceNode& root = state.workspace.root;
|
|
return root.kind == UIEditorWorkspaceNodeKind::TabStack &&
|
|
root.children.size() == 1u &&
|
|
root.children.front().kind == UIEditorWorkspaceNodeKind::Panel &&
|
|
root.children.front().panel.panelId == panelId &&
|
|
state.session.panelStates.size() == 1u &&
|
|
state.session.panelStates.front().panelId == panelId;
|
|
}
|
|
|
|
bool TryExtractPanelFromWindow(
|
|
UIEditorWindowWorkspaceSet& windowSet,
|
|
std::string_view sourceWindowId,
|
|
std::string_view primaryWindowId,
|
|
std::string_view sourceNodeId,
|
|
std::string_view panelId,
|
|
UIEditorWorkspaceExtractedPanel& extractedPanel) {
|
|
UIEditorWindowWorkspaceState* sourceWindow =
|
|
FindMutableUIEditorWindowWorkspaceState(windowSet, sourceWindowId);
|
|
if (sourceWindow == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
if (sourceWindowId != primaryWindowId &&
|
|
IsSinglePanelRootWindow(*sourceWindow, panelId)) {
|
|
extractedPanel.panelNode = std::move(sourceWindow->workspace.root.children.front());
|
|
extractedPanel.sessionState = sourceWindow->session.panelStates.front();
|
|
windowSet.windows.erase(
|
|
std::remove_if(
|
|
windowSet.windows.begin(),
|
|
windowSet.windows.end(),
|
|
[sourceWindowId](const UIEditorWindowWorkspaceState& state) {
|
|
return state.windowId == sourceWindowId;
|
|
}),
|
|
windowSet.windows.end());
|
|
return true;
|
|
}
|
|
|
|
return TryExtractUIEditorWorkspaceVisiblePanel(
|
|
sourceWindow->workspace,
|
|
sourceWindow->session,
|
|
sourceNodeId,
|
|
panelId,
|
|
extractedPanel);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string_view GetUIEditorWindowWorkspaceOperationStatusName(
|
|
UIEditorWindowWorkspaceOperationStatus status) {
|
|
switch (status) {
|
|
case UIEditorWindowWorkspaceOperationStatus::Changed:
|
|
return "Changed";
|
|
case UIEditorWindowWorkspaceOperationStatus::NoOp:
|
|
return "NoOp";
|
|
case UIEditorWindowWorkspaceOperationStatus::Rejected:
|
|
return "Rejected";
|
|
}
|
|
|
|
return "Unknown";
|
|
}
|
|
|
|
UIEditorWindowWorkspaceController::UIEditorWindowWorkspaceController(
|
|
UIEditorPanelRegistry panelRegistry,
|
|
UIEditorWindowWorkspaceSet windowSet)
|
|
: m_panelRegistry(std::move(panelRegistry))
|
|
, m_windowSet(std::move(windowSet)) {
|
|
}
|
|
|
|
UIEditorWindowWorkspaceValidationResult UIEditorWindowWorkspaceController::ValidateState() const {
|
|
return ValidateUIEditorWindowWorkspaceSet(m_panelRegistry, m_windowSet);
|
|
}
|
|
|
|
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus status,
|
|
std::string message,
|
|
std::string_view sourceWindowId,
|
|
std::string_view targetWindowId,
|
|
std::string_view panelId) const {
|
|
UIEditorWindowWorkspaceOperationResult result = {};
|
|
result.status = status;
|
|
result.message = std::move(message);
|
|
result.sourceWindowId = std::string(sourceWindowId);
|
|
result.targetWindowId = std::string(targetWindowId);
|
|
result.panelId = std::string(panelId);
|
|
result.activeWindowId = m_windowSet.activeWindowId;
|
|
result.windowIds.reserve(m_windowSet.windows.size());
|
|
for (const UIEditorWindowWorkspaceState& state : m_windowSet.windows) {
|
|
result.windowIds.push_back(state.windowId);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string UIEditorWindowWorkspaceController::MakeUniqueWindowId(std::string_view base) const {
|
|
std::string resolvedBase = base.empty()
|
|
? std::string("detached-window")
|
|
: std::string(base);
|
|
if (FindUIEditorWindowWorkspaceState(m_windowSet, resolvedBase) == nullptr) {
|
|
return resolvedBase;
|
|
}
|
|
|
|
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
|
|
const std::string candidate = resolvedBase + "-" + std::to_string(suffix);
|
|
if (FindUIEditorWindowWorkspaceState(m_windowSet, candidate) == nullptr) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return resolvedBase + "-overflow";
|
|
}
|
|
|
|
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::DetachPanelToNewWindow(
|
|
std::string_view sourceWindowId,
|
|
std::string_view sourceNodeId,
|
|
std::string_view panelId,
|
|
std::string_view preferredNewWindowId) {
|
|
const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState();
|
|
if (!stateValidation.IsValid()) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Window workspace state invalid: " + stateValidation.message,
|
|
sourceWindowId,
|
|
{},
|
|
panelId);
|
|
}
|
|
|
|
const UIEditorWindowWorkspaceState* sourceWindow =
|
|
FindUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId);
|
|
if (sourceWindow == nullptr) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Source window not found.",
|
|
sourceWindowId,
|
|
{},
|
|
panelId);
|
|
}
|
|
|
|
if (sourceWindowId != m_windowSet.primaryWindowId &&
|
|
IsSinglePanelRootWindow(*sourceWindow, panelId)) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::NoOp,
|
|
"Panel already occupies its own detached window.",
|
|
sourceWindowId,
|
|
sourceWindowId,
|
|
panelId);
|
|
}
|
|
|
|
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
|
UIEditorWorkspaceExtractedPanel extractedPanel = {};
|
|
if (!TryExtractPanelFromWindow(
|
|
m_windowSet,
|
|
sourceWindowId,
|
|
m_windowSet.primaryWindowId,
|
|
sourceNodeId,
|
|
panelId,
|
|
extractedPanel)) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Failed to extract panel from source window.",
|
|
sourceWindowId,
|
|
{},
|
|
panelId);
|
|
}
|
|
|
|
const std::string newWindowId = MakeUniqueWindowId(
|
|
preferredNewWindowId.empty()
|
|
? std::string(panelId) + "-window"
|
|
: std::string(preferredNewWindowId));
|
|
UIEditorWindowWorkspaceState detachedWindow = {};
|
|
detachedWindow.windowId = newWindowId;
|
|
detachedWindow.workspace =
|
|
BuildUIEditorDetachedWorkspaceFromExtractedPanel(
|
|
newWindowId + "-root",
|
|
extractedPanel);
|
|
detachedWindow.session =
|
|
BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(extractedPanel);
|
|
m_windowSet.windows.push_back(std::move(detachedWindow));
|
|
m_windowSet.activeWindowId = newWindowId;
|
|
|
|
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
|
if (!validation.IsValid()) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Detach produced invalid state: " + validation.message,
|
|
sourceWindowId,
|
|
newWindowId,
|
|
panelId);
|
|
}
|
|
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Changed,
|
|
"Panel detached into a new window.",
|
|
sourceWindowId,
|
|
newWindowId,
|
|
panelId);
|
|
}
|
|
|
|
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::MovePanelToStack(
|
|
std::string_view sourceWindowId,
|
|
std::string_view sourceNodeId,
|
|
std::string_view panelId,
|
|
std::string_view targetWindowId,
|
|
std::string_view targetNodeId,
|
|
std::size_t targetVisibleInsertionIndex) {
|
|
const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState();
|
|
if (!stateValidation.IsValid()) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Window workspace state invalid: " + stateValidation.message,
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
if (sourceWindowId == targetWindowId) {
|
|
UIEditorWindowWorkspaceState* window =
|
|
FindMutableUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId);
|
|
if (window == nullptr) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Source window not found.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
if (!TryMoveUIEditorWorkspaceTabToStack(
|
|
window->workspace,
|
|
window->session,
|
|
sourceNodeId,
|
|
panelId,
|
|
targetNodeId,
|
|
targetVisibleInsertionIndex)) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Move operation rejected by the workspace model.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
m_windowSet.activeWindowId = std::string(targetWindowId);
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Changed,
|
|
"Panel moved within the same window.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
|
UIEditorWorkspaceExtractedPanel extractedPanel = {};
|
|
if (!TryExtractPanelFromWindow(
|
|
m_windowSet,
|
|
sourceWindowId,
|
|
m_windowSet.primaryWindowId,
|
|
sourceNodeId,
|
|
panelId,
|
|
extractedPanel)) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Failed to extract panel from source window.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
UIEditorWindowWorkspaceState* targetWindow =
|
|
FindMutableUIEditorWindowWorkspaceState(m_windowSet, targetWindowId);
|
|
if (targetWindow == nullptr ||
|
|
!TryInsertExtractedUIEditorWorkspacePanelToStack(
|
|
targetWindow->workspace,
|
|
targetWindow->session,
|
|
std::move(extractedPanel),
|
|
targetNodeId,
|
|
targetVisibleInsertionIndex)) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Failed to insert the extracted panel into the target stack.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
m_windowSet.activeWindowId = std::string(targetWindowId);
|
|
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
|
if (!validation.IsValid()) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Move produced invalid state: " + validation.message,
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Changed,
|
|
"Panel moved across windows.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::DockPanelRelative(
|
|
std::string_view sourceWindowId,
|
|
std::string_view sourceNodeId,
|
|
std::string_view panelId,
|
|
std::string_view targetWindowId,
|
|
std::string_view targetNodeId,
|
|
UIEditorWorkspaceDockPlacement placement,
|
|
float splitRatio) {
|
|
const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState();
|
|
if (!stateValidation.IsValid()) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Window workspace state invalid: " + stateValidation.message,
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
if (sourceWindowId == targetWindowId) {
|
|
UIEditorWindowWorkspaceState* window =
|
|
FindMutableUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId);
|
|
if (window == nullptr) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Source window not found.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
if (!TryDockUIEditorWorkspaceTabRelative(
|
|
window->workspace,
|
|
window->session,
|
|
sourceNodeId,
|
|
panelId,
|
|
targetNodeId,
|
|
placement,
|
|
splitRatio)) {
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Dock operation rejected by the workspace model.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
m_windowSet.activeWindowId = std::string(targetWindowId);
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Changed,
|
|
"Panel docked within the same window.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
|
UIEditorWorkspaceExtractedPanel extractedPanel = {};
|
|
if (!TryExtractPanelFromWindow(
|
|
m_windowSet,
|
|
sourceWindowId,
|
|
m_windowSet.primaryWindowId,
|
|
sourceNodeId,
|
|
panelId,
|
|
extractedPanel)) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Failed to extract panel from source window.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
UIEditorWindowWorkspaceState* targetWindow =
|
|
FindMutableUIEditorWindowWorkspaceState(m_windowSet, targetWindowId);
|
|
if (targetWindow == nullptr ||
|
|
!TryDockExtractedUIEditorWorkspacePanelRelative(
|
|
targetWindow->workspace,
|
|
targetWindow->session,
|
|
std::move(extractedPanel),
|
|
targetNodeId,
|
|
placement,
|
|
splitRatio)) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Failed to dock the extracted panel into the target window.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
m_windowSet.activeWindowId = std::string(targetWindowId);
|
|
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
|
if (!validation.IsValid()) {
|
|
m_windowSet = windowSetBefore;
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
|
"Dock produced invalid state: " + validation.message,
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
return BuildOperationResult(
|
|
UIEditorWindowWorkspaceOperationStatus::Changed,
|
|
"Panel docked across windows.",
|
|
sourceWindowId,
|
|
targetWindowId,
|
|
panelId);
|
|
}
|
|
|
|
UIEditorWindowWorkspaceController BuildDefaultUIEditorWindowWorkspaceController(
|
|
const UIEditorPanelRegistry& panelRegistry,
|
|
const UIEditorWorkspaceModel& workspace,
|
|
std::string primaryWindowId) {
|
|
return UIEditorWindowWorkspaceController(
|
|
panelRegistry,
|
|
BuildDefaultUIEditorWindowWorkspaceSet(
|
|
panelRegistry,
|
|
workspace,
|
|
std::move(primaryWindowId)));
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor
|