Lay groundwork for detached editor windows
This commit is contained in:
368
docs/plan/NewEditor_Tab脱离独立窗口重构计划_2026-04-14.md
Normal file
368
docs/plan/NewEditor_Tab脱离独立窗口重构计划_2026-04-14.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# NewEditor Tab 脱离独立窗口重构计划
|
||||
|
||||
日期: `2026-04-14`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
把 `new_editor` 当前“只能在主窗口内部重排 / dock”的 tab 系统,重构为支持:
|
||||
|
||||
1. tab 从主窗口拖出后,生成独立窗口
|
||||
2. 独立窗口中的 tab 可以继续拖回主窗口
|
||||
3. 独立窗口之间可以继续相互 dock / 合并
|
||||
4. 现有单窗口 DockHost 布局与 panel 内容代码尽量保持复用
|
||||
|
||||
核心原则:
|
||||
|
||||
1. 不在现有 `UIEditorWorkspaceNodeKind` 里硬塞 `FloatingWindow`
|
||||
2. 保持“一棵 workspace 树只描述一个窗口内部的 dock 布局”
|
||||
3. 浮动窗口是宿主层与窗口集合层的概念,不是 dock 树节点概念
|
||||
4. 先把结构做对,再接拖拽与窗口生命周期
|
||||
|
||||
## 2. 当前根因
|
||||
|
||||
当前不能把 tab 拖成独立窗口,不是交互细节 bug,而是架构缺层。
|
||||
|
||||
现状如下:
|
||||
|
||||
1. `UIEditorWorkspaceModel` 只描述一棵窗口内 dock tree
|
||||
2. `UIEditorDockHostInteraction` 只支持:
|
||||
- `ReorderTab(...)`
|
||||
- `MoveTabToStack(...)`
|
||||
- `DockTabRelative(...)`
|
||||
3. 指针离开 dock host 以后,只会清空 `dropPreview`,不会生成新的宿主窗口实体
|
||||
4. `Application` / `ProductEditorWorkspace` / `ProductEditorContext` 当前都只服务一个主窗口实例
|
||||
|
||||
所以必须先补上“窗口集合 + 每窗口独立 shell 状态 + 跨窗口拖拽转移”这一层。
|
||||
|
||||
## 3. 设计原则
|
||||
|
||||
### 3.1 保留单窗口 workspace 模型
|
||||
|
||||
保留当前这条边界:
|
||||
|
||||
- `UIEditorWorkspaceModel`
|
||||
- `UIEditorWorkspaceController`
|
||||
- `UIEditorDockHost`
|
||||
- `UIEditorDockHostInteraction`
|
||||
|
||||
它们继续只处理“一个窗口内部”的 dock、split、tab、panel。
|
||||
|
||||
### 3.2 新增窗口集合层
|
||||
|
||||
新增一层更高的宿主模型,例如:
|
||||
|
||||
- `UIEditorWindowWorkspaceState`
|
||||
- `UIEditorWindowWorkspaceController`
|
||||
- `UIEditorWindowWorkspaceSet`
|
||||
|
||||
职责是:
|
||||
|
||||
1. 持有主窗口 workspace
|
||||
2. 持有多个 detached window workspace
|
||||
3. 管理跨窗口 tab 转移
|
||||
4. 管理 detached window 的创建、关闭、回收
|
||||
|
||||
### 3.3 拖拽是全局会话,不是单窗口局部状态
|
||||
|
||||
当前 tab 拖拽状态挂在单个 `UIEditorDockHostInteractionState` 里,这只适用于单窗口。
|
||||
|
||||
重构后要拆成两层:
|
||||
|
||||
1. 窗口内局部状态
|
||||
- tab strip hover / active / splitter drag
|
||||
2. 跨窗口全局拖拽状态
|
||||
- 正在拖哪个 panel
|
||||
- 来源窗口 id / 来源 node id
|
||||
- 当前鼠标屏幕坐标
|
||||
- 当前命中的目标窗口 / 目标 tab stack
|
||||
- 是否已经触发“脱离为新窗口”
|
||||
|
||||
### 3.4 panel 内容与业务状态不跟随 HWND 绑死
|
||||
|
||||
`Hierarchy / Inspector / Project / Console / Scene / Game` 的内容逻辑仍然由 editor 业务层统一提供。
|
||||
|
||||
窗口只负责:
|
||||
|
||||
1. 展示哪些 panel
|
||||
2. 当前 panel 在这个窗口里的 dock 关系
|
||||
3. 这个窗口自己的输入、hover、capture、draw data
|
||||
|
||||
## 4. 目标架构
|
||||
|
||||
## 4.1 数据层
|
||||
|
||||
新增建议:
|
||||
|
||||
- `new_editor/include/XCEditor/Shell/UIEditorWindowWorkspaceModel.h`
|
||||
- `new_editor/src/Shell/UIEditorWindowWorkspaceModel.cpp`
|
||||
|
||||
建议结构:
|
||||
|
||||
```cpp
|
||||
struct UIEditorWindowWorkspaceState {
|
||||
std::string windowId;
|
||||
UIEditorWorkspaceModel workspace;
|
||||
};
|
||||
|
||||
struct UIEditorWindowWorkspaceSet {
|
||||
std::string primaryWindowId;
|
||||
std::vector<UIEditorWindowWorkspaceState> windows;
|
||||
std::string activeWindowId;
|
||||
};
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. 每个窗口持有一份自己的 `UIEditorWorkspaceModel`
|
||||
2. 主窗口只是 `primaryWindowId`
|
||||
3. detached window 只是 `windows` 里的非主窗口项
|
||||
|
||||
## 4.2 控制层
|
||||
|
||||
新增建议:
|
||||
|
||||
- `new_editor/include/XCEditor/Shell/UIEditorWindowWorkspaceController.h`
|
||||
- `new_editor/src/Shell/UIEditorWindowWorkspaceController.cpp`
|
||||
|
||||
职责:
|
||||
|
||||
1. 创建 detached window workspace
|
||||
2. 销毁空窗口
|
||||
3. 把 panel 从源窗口移到目标窗口
|
||||
4. 把单 panel 初始化成新窗口根 tab stack
|
||||
5. 支持:
|
||||
- 同窗口重排
|
||||
- 跨窗口移入已有 stack
|
||||
- 跨窗口 dock relative
|
||||
- 拖出生成新窗口
|
||||
|
||||
这里不要复制已有 dock 逻辑,而是复用现有 `UIEditorWorkspaceController` 的布局变换能力。
|
||||
|
||||
## 4.3 交互层
|
||||
|
||||
新增建议:
|
||||
|
||||
- `new_editor/include/XCEditor/Shell/UIEditorWindowDragSession.h`
|
||||
- `new_editor/src/Shell/UIEditorWindowDragSession.cpp`
|
||||
|
||||
全局拖拽状态建议:
|
||||
|
||||
```cpp
|
||||
struct UIEditorWindowTabDragSession {
|
||||
bool active = false;
|
||||
std::string sourceWindowId;
|
||||
std::string sourceNodeId;
|
||||
std::string sourcePanelId;
|
||||
UIPoint screenPointerPosition = {};
|
||||
bool detachedWindowCreated = false;
|
||||
};
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
1. 在某个窗口内开始 tab drag 时,建立全局 drag session
|
||||
2. 鼠标还在某个 dock host 内时,按原有 drop preview 逻辑走
|
||||
3. 鼠标离开所有窗口且超过脱离阈值时,创建新 detached window
|
||||
4. 新窗口创建后,把拖拽中的 panel 挂到这个窗口
|
||||
5. 后续拖拽继续以新窗口为目标窗口参与命中
|
||||
|
||||
## 4.4 宿主层
|
||||
|
||||
当前 `Application` 只对应一个 HWND,需要补成“主窗口 + detached 窗口宿主实例集合”。
|
||||
|
||||
建议新增:
|
||||
|
||||
- `new_editor/app/Host/EditorHostWindow.h`
|
||||
- `new_editor/app/Host/EditorHostWindow.cpp`
|
||||
- `new_editor/app/Host/EditorHostWindowManager.h`
|
||||
- `new_editor/app/Host/EditorHostWindowManager.cpp`
|
||||
|
||||
职责拆分:
|
||||
|
||||
### `EditorHostWindow`
|
||||
|
||||
负责单个 HWND:
|
||||
|
||||
1. Win32 消息
|
||||
2. 单窗口 render loop
|
||||
3. 单窗口 workspace bounds
|
||||
4. 单窗口 shell interaction / draw / cursor / capture
|
||||
|
||||
### `EditorHostWindowManager`
|
||||
|
||||
负责多窗口:
|
||||
|
||||
1. 创建主窗口
|
||||
2. 创建 detached window
|
||||
3. 维护 `windowId -> host window instance`
|
||||
4. 在窗口关闭时回收 state
|
||||
5. 协调全局 tab drag session
|
||||
|
||||
## 5. 执行阶段
|
||||
|
||||
## 阶段 A:先补窗口集合模型,不改宿主
|
||||
|
||||
### 目标
|
||||
|
||||
先把“一个窗口集合里有多棵 workspace”建立起来,但仍然只跑主窗口。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 新增 `UIEditorWindowWorkspaceSet`
|
||||
2. 新增集合级 controller
|
||||
3. 实现:
|
||||
- `DetachPanelToNewWindow(...)`
|
||||
- `MovePanelBetweenWindowsToStack(...)`
|
||||
- `DockPanelBetweenWindowsRelative(...)`
|
||||
4. 给这些操作补基础单元测试
|
||||
|
||||
### 验收
|
||||
|
||||
1. 不开第二个 HWND,也能在纯数据层完成 panel 跨窗口迁移
|
||||
2. 源窗口 panel 被正确移除
|
||||
3. 目标窗口 root 布局正确生成
|
||||
|
||||
## 阶段 B:把单窗口宿主抽成可复用的 window instance
|
||||
|
||||
### 目标
|
||||
|
||||
把当前 `Application` 里只适用于单 HWND 的状态拆出来,为多窗口做准备。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 提取单窗口运行时状态
|
||||
2. 提取单窗口 render/update/append/input 流程
|
||||
3. 让主窗口也走 `EditorHostWindow`
|
||||
|
||||
### 验收
|
||||
|
||||
1. 主窗口行为不回退
|
||||
2. 代码里不再默认“全局只有一个 workspace / 一个 shell state / 一个 hwnd”
|
||||
|
||||
## 阶段 C:接入 detached window 的 HWND 生命周期
|
||||
|
||||
### 目标
|
||||
|
||||
真正把第二个窗口创建出来,但先不接完整跨窗口拖拽。
|
||||
|
||||
### 任务
|
||||
|
||||
1. `EditorHostWindowManager` 支持创建第二个 `EditorHostWindow`
|
||||
2. detached window 拥有自己的:
|
||||
- HWND
|
||||
- render loop
|
||||
- interaction state
|
||||
- draw pass
|
||||
3. 空窗口关闭回收
|
||||
|
||||
### 验收
|
||||
|
||||
1. 可以通过代码直接创建一个带单 panel 的 detached window
|
||||
2. 该窗口可独立显示、移动、关闭
|
||||
|
||||
## 阶段 D:接入 tab 拖出创建 detached window
|
||||
|
||||
### 目标
|
||||
|
||||
把“拖出”真正打通。
|
||||
|
||||
### 任务
|
||||
|
||||
1. tab drag 启动时创建全局 drag session
|
||||
2. 当指针离开全部 dock host 且满足阈值时:
|
||||
- 从源窗口移除 panel
|
||||
- 创建 detached window model
|
||||
- 创建 detached HWND
|
||||
3. 新窗口初始位置跟随拖拽起点与屏幕坐标
|
||||
|
||||
### 验收
|
||||
|
||||
1. 从主窗口拖出任意 tab,可变成独立窗口
|
||||
2. 不是复制,是迁移
|
||||
3. 原窗口布局自动 canonicalize,不留下空 stack / 空 split
|
||||
|
||||
## 阶段 E:接入跨窗口 re-dock
|
||||
|
||||
### 目标
|
||||
|
||||
支持 detached window 和主窗口双向回 dock。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 命中其他窗口 dock host 时生成目标窗口 drop preview
|
||||
2. pointer up 时执行跨窗口:
|
||||
- move to stack
|
||||
- dock relative
|
||||
3. 如果源窗口被拖空,自动关闭该 detached window
|
||||
|
||||
### 验收
|
||||
|
||||
1. detached -> main 可回 dock
|
||||
2. detached -> detached 可互相 dock
|
||||
3. 空窗口自动销毁,非空窗口保持
|
||||
|
||||
## 阶段 F:补交互与收口
|
||||
|
||||
### 目标
|
||||
|
||||
把功能补到可长期使用,不留明显交互破洞。
|
||||
|
||||
### 任务
|
||||
|
||||
1. 处理激活窗口切换
|
||||
2. 处理 capture / focus / cursor 所有权
|
||||
3. 处理窗口关闭时的 panel 回收策略
|
||||
4. 预留布局持久化结构
|
||||
|
||||
### 验收
|
||||
|
||||
1. 多窗口拖拽不闪退
|
||||
2. capture 不串窗口
|
||||
3. focus / active panel 行为稳定
|
||||
|
||||
## 6. 关键风险
|
||||
|
||||
### 6.1 不能让 panel 业务实例被窗口复制
|
||||
|
||||
如果直接复制 `ProductProjectPanel / ProductHierarchyPanel` 这类对象,很容易出现双状态漂移。
|
||||
|
||||
要先确认:
|
||||
|
||||
1. 哪些状态是全局业务状态
|
||||
2. 哪些状态是窗口内展示状态
|
||||
|
||||
### 6.2 指针捕获要统一到宿主管理层
|
||||
|
||||
当前 capture 大多按单窗口处理。多窗口后如果还各管各的,很容易出现:
|
||||
|
||||
1. 源窗口还认为自己在 drag
|
||||
2. 目标窗口已经开始 preview
|
||||
3. 鼠标松开后状态残留
|
||||
|
||||
所以 drag session 与 capture 仲裁必须提升到 window manager。
|
||||
|
||||
### 6.3 不能把跨窗口逻辑塞回 DockHostInteraction
|
||||
|
||||
`UIEditorDockHostInteraction` 继续只处理单窗口内部命中与 preview。
|
||||
|
||||
跨窗口逻辑应放在更上层,否则很快会变成屎山。
|
||||
|
||||
## 7. 本轮执行顺序
|
||||
|
||||
这一轮按下面顺序推进:
|
||||
|
||||
1. 阶段 A:补窗口集合模型与 controller
|
||||
2. 阶段 B:提取单窗口 host instance
|
||||
3. 阶段 C:先用代码路径创建 detached window,打通多 HWND
|
||||
4. 阶段 D:接 tab 拖出创建新窗口
|
||||
5. 阶段 E:接跨窗口 re-dock
|
||||
|
||||
## 8. 收口标准
|
||||
|
||||
满足以下条件,才算这次重构收口:
|
||||
|
||||
1. tab 可以从主窗口拖出成为独立窗口
|
||||
2. 独立窗口可以拖回主窗口
|
||||
3. 独立窗口之间可以继续 dock
|
||||
4. 不出现空窗口残留、空 stack 残留、拖拽状态残留
|
||||
5. 不破坏当前主窗口已有的 tab 重排 / split / dock 功能
|
||||
@@ -89,6 +89,9 @@ set(XCUI_EDITOR_SHELL_SOURCES
|
||||
src/Shell/UIEditorWorkspaceLayoutPersistence.cpp
|
||||
src/Shell/UIEditorWorkspaceModel.cpp
|
||||
src/Shell/UIEditorWorkspaceSession.cpp
|
||||
src/Shell/UIEditorWorkspaceTransfer.cpp
|
||||
src/Shell/UIEditorWindowWorkspaceController.cpp
|
||||
src/Shell/UIEditorWindowWorkspaceModel.cpp
|
||||
)
|
||||
|
||||
set(XCUI_EDITOR_WIDGET_SUPPORT_SOURCES
|
||||
|
||||
@@ -467,6 +467,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
"shell asset validation failed: " + m_editorContext.GetValidationMessage());
|
||||
return false;
|
||||
}
|
||||
m_workspaceController = m_editorContext.BuildWorkspaceController();
|
||||
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
@@ -568,7 +569,9 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
LogRuntimeTrace(
|
||||
"app",
|
||||
"workspace initialized: " +
|
||||
m_editorContext.DescribeWorkspaceState(m_editorWorkspace.GetShellInteractionState()));
|
||||
m_editorContext.DescribeWorkspaceState(
|
||||
m_workspaceController,
|
||||
m_editorWorkspace.GetShellInteractionState()));
|
||||
m_renderReady = true;
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
@@ -639,6 +642,7 @@ void Application::RenderFrame() {
|
||||
"input",
|
||||
DescribeInputEvents(frameEvents) + " | " +
|
||||
m_editorContext.DescribeWorkspaceState(
|
||||
m_workspaceController,
|
||||
m_editorWorkspace.GetShellInteractionState()));
|
||||
}
|
||||
|
||||
@@ -650,6 +654,7 @@ void Application::RenderFrame() {
|
||||
|
||||
m_editorWorkspace.Update(
|
||||
m_editorContext,
|
||||
m_workspaceController,
|
||||
workspaceBounds,
|
||||
frameEvents,
|
||||
BuildCaptureStatusText());
|
||||
@@ -667,7 +672,7 @@ void Application::RenderFrame() {
|
||||
<< " commandExecuted="
|
||||
<< (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false")
|
||||
<< " active="
|
||||
<< m_editorContext.GetWorkspaceController().GetWorkspace().activePanelId
|
||||
<< m_workspaceController.GetWorkspace().activePanelId
|
||||
<< " message="
|
||||
<< shellFrame.result.workspaceResult.dockHostResult.layoutResult.message;
|
||||
LogRuntimeTrace(
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Workspace/ProductEditorWorkspace.h"
|
||||
|
||||
#include <XCEditor/Shell/UIEditorShellInteraction.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
@@ -126,6 +127,7 @@ private:
|
||||
::XCEngine::UI::Editor::Host::AutoScreenshotController m_autoScreenshot = {};
|
||||
::XCEngine::UI::Editor::Host::InputModifierTracker m_inputModifierTracker = {};
|
||||
App::ProductEditorContext m_editorContext = {};
|
||||
UIEditorWorkspaceController m_workspaceController = {};
|
||||
App::ProductEditorWorkspace m_editorWorkspace = {};
|
||||
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||
bool m_trackingMouseLeave = false;
|
||||
|
||||
@@ -39,11 +39,6 @@ bool ProductEditorContext::Initialize(const std::filesystem::path& repoRoot) {
|
||||
m_session = {};
|
||||
m_session.repoRoot = repoRoot;
|
||||
m_session.projectRoot = (repoRoot / "project").lexically_normal();
|
||||
m_workspaceController = UIEditorWorkspaceController(
|
||||
m_shellAsset.panelRegistry,
|
||||
m_shellAsset.workspace,
|
||||
m_shellAsset.workspaceSession);
|
||||
SyncSessionFromWorkspace();
|
||||
m_hostCommandBridge.BindSession(m_session);
|
||||
m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset);
|
||||
m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge);
|
||||
@@ -63,8 +58,9 @@ void ProductEditorContext::SetExitRequestHandler(std::function<void()> handler)
|
||||
m_hostCommandBridge.SetExitRequestHandler(std::move(handler));
|
||||
}
|
||||
|
||||
void ProductEditorContext::SyncSessionFromWorkspace() {
|
||||
SyncProductEditorSessionFromWorkspace(m_session, m_workspaceController);
|
||||
void ProductEditorContext::SyncSessionFromWorkspace(
|
||||
const UIEditorWorkspaceController& workspaceController) {
|
||||
SyncProductEditorSessionFromWorkspace(m_session, workspaceController);
|
||||
}
|
||||
|
||||
bool ProductEditorContext::IsValid() const {
|
||||
@@ -91,12 +87,11 @@ void ProductEditorContext::ClearSelection() {
|
||||
m_session.selection = {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() {
|
||||
return m_workspaceController;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() const {
|
||||
return m_workspaceController;
|
||||
UIEditorWorkspaceController ProductEditorContext::BuildWorkspaceController() const {
|
||||
return UIEditorWorkspaceController(
|
||||
m_shellAsset.panelRegistry,
|
||||
m_shellAsset.workspace,
|
||||
m_shellAsset.workspaceSession);
|
||||
}
|
||||
|
||||
const UIEditorShellInteractionServices& ProductEditorContext::GetShellServices() const {
|
||||
@@ -104,10 +99,11 @@ const UIEditorShellInteractionServices& ProductEditorContext::GetShellServices()
|
||||
}
|
||||
|
||||
UIEditorShellInteractionDefinition ProductEditorContext::BuildShellDefinition(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
std::string_view captureText) const {
|
||||
return BuildProductShellInteractionDefinition(
|
||||
m_shellAsset,
|
||||
m_workspaceController,
|
||||
workspaceController,
|
||||
ComposeStatusText(m_lastStatus, m_lastMessage),
|
||||
captureText);
|
||||
}
|
||||
@@ -127,6 +123,7 @@ void ProductEditorContext::SetStatus(
|
||||
}
|
||||
|
||||
void ProductEditorContext::UpdateStatusFromShellResult(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
const UIEditorShellInteractionResult& result) {
|
||||
if (result.commandDispatched) {
|
||||
SetStatus(
|
||||
@@ -197,13 +194,14 @@ void ProductEditorContext::AppendConsoleEntry(
|
||||
}
|
||||
|
||||
std::string ProductEditorContext::DescribeWorkspaceState(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
const UIEditorShellInteractionState& interactionState) const {
|
||||
std::ostringstream stream = {};
|
||||
stream << "active=" << m_workspaceController.GetWorkspace().activePanelId;
|
||||
stream << "active=" << workspaceController.GetWorkspace().activePanelId;
|
||||
const auto visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(
|
||||
m_workspaceController.GetWorkspace(),
|
||||
m_workspaceController.GetSession());
|
||||
workspaceController.GetWorkspace(),
|
||||
workspaceController.GetSession());
|
||||
stream << " visible=[";
|
||||
for (std::size_t index = 0; index < visiblePanels.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
bool Initialize(const std::filesystem::path& repoRoot);
|
||||
void AttachTextMeasurer(const UIEditorTextMeasurer& textMeasurer);
|
||||
void SetExitRequestHandler(std::function<void()> handler);
|
||||
void SyncSessionFromWorkspace();
|
||||
void SyncSessionFromWorkspace(const UIEditorWorkspaceController& workspaceController);
|
||||
|
||||
bool IsValid() const;
|
||||
const std::string& GetValidationMessage() const;
|
||||
@@ -29,17 +29,20 @@ public:
|
||||
void SetSelection(ProductEditorSelectionState selection);
|
||||
void ClearSelection();
|
||||
|
||||
UIEditorWorkspaceController& GetWorkspaceController();
|
||||
const UIEditorWorkspaceController& GetWorkspaceController() const;
|
||||
UIEditorWorkspaceController BuildWorkspaceController() const;
|
||||
const UIEditorShellInteractionServices& GetShellServices() const;
|
||||
|
||||
UIEditorShellInteractionDefinition BuildShellDefinition(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
std::string_view captureText) const;
|
||||
|
||||
void SetReadyStatus();
|
||||
void SetStatus(std::string status, std::string message);
|
||||
void UpdateStatusFromShellResult(const UIEditorShellInteractionResult& result);
|
||||
void UpdateStatusFromShellResult(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
const UIEditorShellInteractionResult& result);
|
||||
std::string DescribeWorkspaceState(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
const UIEditorShellInteractionState& interactionState) const;
|
||||
|
||||
private:
|
||||
@@ -47,7 +50,6 @@ private:
|
||||
|
||||
EditorShellAsset m_shellAsset = {};
|
||||
EditorShellAssetValidationResult m_shellValidation = {};
|
||||
UIEditorWorkspaceController m_workspaceController = {};
|
||||
UIEditorShortcutManager m_shortcutManager = {};
|
||||
UIEditorShellInteractionServices m_shellServices = {};
|
||||
ProductEditorSession m_session = {};
|
||||
|
||||
@@ -250,13 +250,14 @@ void ProductEditorWorkspace::Shutdown() {
|
||||
|
||||
void ProductEditorWorkspace::Update(
|
||||
ProductEditorContext& context,
|
||||
UIEditorWorkspaceController& workspaceController,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
std::string_view captureText) {
|
||||
const auto& metrics = ResolveUIEditorShellInteractionMetrics();
|
||||
context.SyncSessionFromWorkspace();
|
||||
context.SyncSessionFromWorkspace(workspaceController);
|
||||
UIEditorShellInteractionDefinition definition =
|
||||
context.BuildShellDefinition(captureText);
|
||||
context.BuildShellDefinition(workspaceController, captureText);
|
||||
m_viewportHostService.BeginFrame();
|
||||
definition.workspacePresentations = BuildWorkspacePresentations(definition);
|
||||
const std::vector<UIInputEvent> shellEvents =
|
||||
@@ -266,7 +267,7 @@ void ProductEditorWorkspace::Update(
|
||||
|
||||
m_shellFrame = UpdateUIEditorShellInteraction(
|
||||
m_shellInteractionState,
|
||||
context.GetWorkspaceController(),
|
||||
workspaceController,
|
||||
bounds,
|
||||
definition,
|
||||
shellEvents,
|
||||
@@ -279,11 +280,11 @@ void ProductEditorWorkspace::Update(
|
||||
inputEvents,
|
||||
shellOwnsHostedContentPointerStream);
|
||||
ApplyViewportFramesToShellFrame(m_shellFrame, m_viewportHostService);
|
||||
context.SyncSessionFromWorkspace();
|
||||
context.UpdateStatusFromShellResult(m_shellFrame.result);
|
||||
context.SyncSessionFromWorkspace(workspaceController);
|
||||
context.UpdateStatusFromShellResult(workspaceController, m_shellFrame.result);
|
||||
|
||||
const std::string& activePanelId =
|
||||
context.GetWorkspaceController().GetWorkspace().activePanelId;
|
||||
workspaceController.GetWorkspace().activePanelId;
|
||||
m_hierarchyPanel.Update(
|
||||
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
|
||||
hostedContentEvents,
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
|
||||
void Update(
|
||||
ProductEditorContext& context,
|
||||
UIEditorWorkspaceController& workspaceController,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
std::string_view captureText);
|
||||
|
||||
@@ -31,10 +31,13 @@ struct UIEditorDockHostInteractionResult {
|
||||
bool consumed = false;
|
||||
bool commandExecuted = false;
|
||||
bool layoutChanged = false;
|
||||
bool detachRequested = false;
|
||||
bool requestPointerCapture = false;
|
||||
bool releasePointerCapture = false;
|
||||
Widgets::UIEditorDockHostHitTarget hitTarget = {};
|
||||
std::string activeSplitterNodeId = {};
|
||||
std::string detachedNodeId = {};
|
||||
std::string detachedPanelId = {};
|
||||
UIEditorWorkspaceCommandResult commandResult = {};
|
||||
UIEditorWorkspaceLayoutOperationResult layoutResult = {};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Shell/UIEditorWindowWorkspaceModel.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
enum class UIEditorWindowWorkspaceOperationStatus : std::uint8_t {
|
||||
Changed = 0,
|
||||
NoOp,
|
||||
Rejected
|
||||
};
|
||||
|
||||
struct UIEditorWindowWorkspaceOperationResult {
|
||||
UIEditorWindowWorkspaceOperationStatus status =
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected;
|
||||
std::string message = {};
|
||||
std::string sourceWindowId = {};
|
||||
std::string targetWindowId = {};
|
||||
std::string panelId = {};
|
||||
std::string activeWindowId = {};
|
||||
std::vector<std::string> windowIds = {};
|
||||
};
|
||||
|
||||
std::string_view GetUIEditorWindowWorkspaceOperationStatusName(
|
||||
UIEditorWindowWorkspaceOperationStatus status);
|
||||
|
||||
class UIEditorWindowWorkspaceController {
|
||||
public:
|
||||
UIEditorWindowWorkspaceController() = default;
|
||||
UIEditorWindowWorkspaceController(
|
||||
UIEditorPanelRegistry panelRegistry,
|
||||
UIEditorWindowWorkspaceSet windowSet);
|
||||
|
||||
const UIEditorPanelRegistry& GetPanelRegistry() const {
|
||||
return m_panelRegistry;
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceSet& GetWindowSet() const {
|
||||
return m_windowSet;
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult ValidateState() const;
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult DetachPanelToNewWindow(
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view preferredNewWindowId = {});
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult 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);
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult 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 = 0.5f);
|
||||
|
||||
private:
|
||||
UIEditorWindowWorkspaceOperationResult BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus status,
|
||||
std::string message,
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view targetWindowId,
|
||||
std::string_view panelId) const;
|
||||
|
||||
std::string MakeUniqueWindowId(std::string_view base) const;
|
||||
|
||||
UIEditorPanelRegistry m_panelRegistry = {};
|
||||
UIEditorWindowWorkspaceSet m_windowSet = {};
|
||||
};
|
||||
|
||||
UIEditorWindowWorkspaceController BuildDefaultUIEditorWindowWorkspaceController(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string primaryWindowId = "main-window");
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorWindowWorkspaceState {
|
||||
std::string windowId = {};
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
UIEditorWorkspaceSession session = {};
|
||||
};
|
||||
|
||||
struct UIEditorWindowWorkspaceSet {
|
||||
std::string primaryWindowId = {};
|
||||
std::string activeWindowId = {};
|
||||
std::vector<UIEditorWindowWorkspaceState> windows = {};
|
||||
};
|
||||
|
||||
enum class UIEditorWindowWorkspaceValidationCode : std::uint8_t {
|
||||
None = 0,
|
||||
InvalidPanelRegistry,
|
||||
EmptyWindowId,
|
||||
DuplicateWindowId,
|
||||
MissingPrimaryWindow,
|
||||
MissingActiveWindow,
|
||||
InvalidWorkspace,
|
||||
InvalidSession
|
||||
};
|
||||
|
||||
struct UIEditorWindowWorkspaceValidationResult {
|
||||
UIEditorWindowWorkspaceValidationCode code = UIEditorWindowWorkspaceValidationCode::None;
|
||||
std::string message = {};
|
||||
|
||||
[[nodiscard]] bool IsValid() const {
|
||||
return code == UIEditorWindowWorkspaceValidationCode::None;
|
||||
}
|
||||
};
|
||||
|
||||
UIEditorWindowWorkspaceSet BuildDefaultUIEditorWindowWorkspaceSet(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string primaryWindowId = "main-window");
|
||||
|
||||
const UIEditorWindowWorkspaceState* FindUIEditorWindowWorkspaceState(
|
||||
const UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view windowId);
|
||||
|
||||
UIEditorWindowWorkspaceState* FindMutableUIEditorWindowWorkspaceState(
|
||||
UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view windowId);
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult ValidateUIEditorWindowWorkspaceSet(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWindowWorkspaceSet& windowSet);
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -150,6 +150,28 @@ bool TryReorderUIEditorWorkspaceTab(
|
||||
std::string_view panelId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryExtractUIEditorWorkspaceVisiblePanelNode(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceNode& extractedPanel);
|
||||
|
||||
bool TryInsertUIEditorWorkspacePanelNodeToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceNode panelNode,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryDockUIEditorWorkspacePanelNodeRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceNode panelNode,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio = 0.5f);
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct UIEditorWorkspaceExtractedPanel {
|
||||
UIEditorWorkspaceNode panelNode = {};
|
||||
UIEditorPanelSessionState sessionState = {};
|
||||
};
|
||||
|
||||
bool TryExtractUIEditorWorkspaceVisiblePanel(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceExtractedPanel& extractedPanel);
|
||||
|
||||
UIEditorWorkspaceModel BuildUIEditorDetachedWorkspaceFromExtractedPanel(
|
||||
std::string rootNodeId,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel);
|
||||
|
||||
UIEditorWorkspaceSession BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel);
|
||||
|
||||
bool TryInsertExtractedUIEditorWorkspacePanelToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
bool TryDockExtractedUIEditorWorkspacePanelRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio = 0.5f);
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -781,6 +781,21 @@ UIEditorDockHostInteractionFrame UpdateUIEditorDockHostInteraction(
|
||||
break;
|
||||
}
|
||||
|
||||
if (!state.activeTabDragNodeId.empty() &&
|
||||
!state.activeTabDragPanelId.empty() &&
|
||||
state.hasPointerPosition &&
|
||||
!IsPointInsideRect(layout.bounds, state.pointerPosition)) {
|
||||
eventResult.detachRequested = true;
|
||||
eventResult.consumed = true;
|
||||
eventResult.detachedNodeId = state.activeTabDragNodeId;
|
||||
eventResult.detachedPanelId = state.activeTabDragPanelId;
|
||||
eventResult.releasePointerCapture =
|
||||
eventResult.releasePointerCapture || tabStripResult.releasePointerCapture;
|
||||
ClearTabDockDragState(state);
|
||||
state.dockHostState.focused = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tabStripResult.dragEnded || tabStripResult.dragCanceled) {
|
||||
eventResult.consumed = tabStripResult.consumed;
|
||||
eventResult.hitTarget = tabStripResult.hitTarget;
|
||||
|
||||
446
new_editor/src/Shell/UIEditorWindowWorkspaceController.cpp
Normal file
446
new_editor/src/Shell/UIEditorWindowWorkspaceController.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include <XCEditor/Shell/UIEditorWindowWorkspaceController.h>
|
||||
|
||||
#include <XCEditor/Shell/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
|
||||
145
new_editor/src/Shell/UIEditorWindowWorkspaceModel.cpp
Normal file
145
new_editor/src/Shell/UIEditorWindowWorkspaceModel.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
#include <XCEditor/Shell/UIEditorWindowWorkspaceModel.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorWindowWorkspaceValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWindowWorkspaceSet BuildDefaultUIEditorWindowWorkspaceSet(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string primaryWindowId) {
|
||||
if (primaryWindowId.empty()) {
|
||||
primaryWindowId = "main-window";
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceSet windowSet = {};
|
||||
windowSet.primaryWindowId = primaryWindowId;
|
||||
windowSet.activeWindowId = primaryWindowId;
|
||||
|
||||
UIEditorWindowWorkspaceState state = {};
|
||||
state.windowId = primaryWindowId;
|
||||
state.workspace = CanonicalizeUIEditorWorkspaceModel(workspace);
|
||||
state.session = BuildDefaultUIEditorWorkspaceSession(panelRegistry, state.workspace);
|
||||
windowSet.windows.push_back(std::move(state));
|
||||
return windowSet;
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceState* FindUIEditorWindowWorkspaceState(
|
||||
const UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view windowId) {
|
||||
for (const UIEditorWindowWorkspaceState& state : windowSet.windows) {
|
||||
if (state.windowId == windowId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceState* FindMutableUIEditorWindowWorkspaceState(
|
||||
UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view windowId) {
|
||||
for (UIEditorWindowWorkspaceState& state : windowSet.windows) {
|
||||
if (state.windowId == windowId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult ValidateUIEditorWindowWorkspaceSet(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWindowWorkspaceSet& windowSet) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::InvalidPanelRegistry,
|
||||
registryValidation.message);
|
||||
}
|
||||
|
||||
if (windowSet.primaryWindowId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingPrimaryWindow,
|
||||
"Primary window id must not be empty.");
|
||||
}
|
||||
|
||||
if (windowSet.activeWindowId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingActiveWindow,
|
||||
"Active window id must not be empty.");
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seenWindowIds = {};
|
||||
bool hasPrimaryWindow = false;
|
||||
bool hasActiveWindow = false;
|
||||
for (const UIEditorWindowWorkspaceState& state : windowSet.windows) {
|
||||
if (state.windowId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::EmptyWindowId,
|
||||
"Window id must not be empty.");
|
||||
}
|
||||
|
||||
if (!seenWindowIds.insert(state.windowId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::DuplicateWindowId,
|
||||
"Window id '" + state.windowId + "' is duplicated.");
|
||||
}
|
||||
|
||||
if (state.windowId == windowSet.primaryWindowId) {
|
||||
hasPrimaryWindow = true;
|
||||
}
|
||||
if (state.windowId == windowSet.activeWindowId) {
|
||||
hasActiveWindow = true;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(state.workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::InvalidWorkspace,
|
||||
"Window '" + state.windowId + "' workspace invalid: " +
|
||||
workspaceValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(panelRegistry, state.workspace, state.session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::InvalidSession,
|
||||
"Window '" + state.windowId + "' session invalid: " +
|
||||
sessionValidation.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPrimaryWindow) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingPrimaryWindow,
|
||||
"Primary window '" + windowSet.primaryWindowId + "' does not exist.");
|
||||
}
|
||||
|
||||
if (!hasActiveWindow) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingActiveWindow,
|
||||
"Active window '" + windowSet.activeWindowId + "' does not exist.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -839,6 +839,145 @@ bool TryReorderUIEditorWorkspaceTab(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryExtractUIEditorWorkspaceVisiblePanelNode(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceNode& extractedPanel) {
|
||||
return TryExtractVisiblePanelFromTabStack(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel);
|
||||
}
|
||||
|
||||
bool TryInsertUIEditorWorkspacePanelNodeToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceNode panelNode,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
if (targetNodeId.empty() ||
|
||||
panelNode.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
panelNode.panel.panelId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string movedPanelId = panelNode.panel.panelId;
|
||||
const std::size_t actualInsertionIndex =
|
||||
ResolveActualInsertionIndexForVisibleInsertion(
|
||||
*targetStack,
|
||||
session,
|
||||
targetVisibleInsertionIndex);
|
||||
if (actualInsertionIndex > targetStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetStack->children.insert(
|
||||
targetStack->children.begin() +
|
||||
static_cast<std::ptrdiff_t>(actualInsertionIndex),
|
||||
std::move(panelNode));
|
||||
targetStack->selectedTabIndex = actualInsertionIndex;
|
||||
workspace.activePanelId = movedPanelId;
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryDockUIEditorWorkspacePanelNodeRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceNode panelNode,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
if (targetNodeId.empty() ||
|
||||
panelNode.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
panelNode.panel.panelId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* targetNode =
|
||||
FindUIEditorWorkspaceNode(workspace, targetNodeId);
|
||||
if (targetNode == nullptr ||
|
||||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (placement == UIEditorWorkspaceDockPlacement::Center) {
|
||||
return TryInsertUIEditorWorkspacePanelNodeToStack(
|
||||
workspace,
|
||||
session,
|
||||
std::move(panelNode),
|
||||
targetNodeId,
|
||||
CountVisibleChildren(*targetNode, session));
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string movedPanelId = panelNode.panel.panelId;
|
||||
const std::string movedStackNodeId = MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_" + movedPanelId + "_stack");
|
||||
UIEditorWorkspaceNode movedStack = {};
|
||||
movedStack.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
movedStack.nodeId = movedStackNodeId;
|
||||
movedStack.selectedTabIndex = 0u;
|
||||
movedStack.children.push_back(std::move(panelNode));
|
||||
|
||||
UIEditorWorkspaceNode existingTarget = std::move(*targetStack);
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (IsLeadingDockPlacement(placement)) {
|
||||
primary = std::move(movedStack);
|
||||
secondary = std::move(existingTarget);
|
||||
} else {
|
||||
primary = std::move(existingTarget);
|
||||
secondary = std::move(movedStack);
|
||||
}
|
||||
|
||||
const float requestedRatio = ClampDockSplitRatio(splitRatio);
|
||||
const float resolvedSplitRatio =
|
||||
IsLeadingDockPlacement(placement)
|
||||
? requestedRatio
|
||||
: (1.0f - requestedRatio);
|
||||
*targetStack = BuildUIEditorWorkspaceSplit(
|
||||
MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_split"),
|
||||
ResolveDockSplitAxis(placement),
|
||||
resolvedSplitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
workspace.activePanelId = movedPanelId;
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
@@ -882,30 +1021,12 @@ bool TryMoveUIEditorWorkspaceTabToStack(
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t actualInsertionIndex =
|
||||
ResolveActualInsertionIndexForVisibleInsertion(
|
||||
*targetStack,
|
||||
session,
|
||||
targetVisibleInsertionIndex);
|
||||
if (actualInsertionIndex > targetStack->children.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetStack->children.insert(
|
||||
targetStack->children.begin() +
|
||||
static_cast<std::ptrdiff_t>(actualInsertionIndex),
|
||||
std::move(extractedPanel));
|
||||
targetStack->selectedTabIndex = actualInsertionIndex;
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
return TryInsertUIEditorWorkspacePanelNodeToStack(
|
||||
workspace,
|
||||
session,
|
||||
std::move(extractedPanel),
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex);
|
||||
}
|
||||
|
||||
bool TryDockUIEditorWorkspaceTabRelative(
|
||||
@@ -965,49 +1086,13 @@ bool TryDockUIEditorWorkspaceTabRelative(
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode* targetStack =
|
||||
FindMutableNodeRecursive(workspace.root, targetNodeId);
|
||||
if (targetStack == nullptr ||
|
||||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string movedStackNodeId = MakeUniqueNodeId(
|
||||
return TryDockUIEditorWorkspacePanelNodeRelative(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_" + std::string(panelId) + "_stack");
|
||||
UIEditorWorkspaceNode movedStack = {};
|
||||
movedStack.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
movedStack.nodeId = movedStackNodeId;
|
||||
movedStack.selectedTabIndex = 0u;
|
||||
movedStack.children.push_back(std::move(extractedPanel));
|
||||
|
||||
UIEditorWorkspaceNode existingTarget = std::move(*targetStack);
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (IsLeadingDockPlacement(placement)) {
|
||||
primary = std::move(movedStack);
|
||||
secondary = std::move(existingTarget);
|
||||
} else {
|
||||
primary = std::move(existingTarget);
|
||||
secondary = std::move(movedStack);
|
||||
}
|
||||
|
||||
const float requestedRatio = ClampDockSplitRatio(splitRatio);
|
||||
const float resolvedSplitRatio =
|
||||
IsLeadingDockPlacement(placement)
|
||||
? requestedRatio
|
||||
: (1.0f - requestedRatio);
|
||||
*targetStack = BuildUIEditorWorkspaceSplit(
|
||||
MakeUniqueNodeId(
|
||||
workspace,
|
||||
std::string(targetNodeId) + "__dock_split"),
|
||||
ResolveDockSplitAxis(placement),
|
||||
resolvedSplitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
workspace.activePanelId = std::string(panelId);
|
||||
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
||||
return true;
|
||||
session,
|
||||
std::move(extractedPanel),
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
209
new_editor/src/Shell/UIEditorWorkspaceTransfer.cpp
Normal file
209
new_editor/src/Shell/UIEditorWorkspaceTransfer.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include <XCEditor/Shell/UIEditorWorkspaceTransfer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
void CollectWorkspacePanelIdsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::vector<std::string>& outPanelIds) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
outPanelIds.push_back(node.panel.panelId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectWorkspacePanelIdsRecursive(child, outPanelIds);
|
||||
}
|
||||
}
|
||||
|
||||
void ReorderSessionStatesToMatchWorkspace(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session) {
|
||||
std::vector<std::string> orderedPanelIds = {};
|
||||
CollectWorkspacePanelIdsRecursive(workspace.root, orderedPanelIds);
|
||||
|
||||
std::vector<UIEditorPanelSessionState> orderedStates = {};
|
||||
orderedStates.reserve(orderedPanelIds.size());
|
||||
for (const std::string& panelId : orderedPanelIds) {
|
||||
auto it = std::find_if(
|
||||
session.panelStates.begin(),
|
||||
session.panelStates.end(),
|
||||
[&panelId](const UIEditorPanelSessionState& state) {
|
||||
return state.panelId == panelId;
|
||||
});
|
||||
if (it != session.panelStates.end()) {
|
||||
orderedStates.push_back(*it);
|
||||
}
|
||||
}
|
||||
|
||||
session.panelStates = std::move(orderedStates);
|
||||
}
|
||||
|
||||
void NormalizeActivePanelAfterMutation(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view preferredPanelId) {
|
||||
if (!preferredPanelId.empty() &&
|
||||
IsPanelOpenAndVisible(session, preferredPanelId) &&
|
||||
ContainsUIEditorWorkspacePanel(workspace, preferredPanelId)) {
|
||||
TryActivateUIEditorWorkspacePanel(workspace, preferredPanelId);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
if (visiblePanels.empty()) {
|
||||
workspace.activePanelId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
TryActivateUIEditorWorkspacePanel(workspace, visiblePanels.front().panelId);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryExtractUIEditorWorkspaceVisiblePanel(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceExtractedPanel& extractedPanel) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorWorkspaceNode panelNode = {};
|
||||
if (!TryExtractUIEditorWorkspaceVisiblePanelNode(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
panelNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto sessionIt = std::find_if(
|
||||
session.panelStates.begin(),
|
||||
session.panelStates.end(),
|
||||
[panelId](const UIEditorPanelSessionState& state) {
|
||||
return state.panelId == panelId;
|
||||
});
|
||||
if (sessionIt == session.panelStates.end()) {
|
||||
workspace = workspaceBefore;
|
||||
session = sessionBefore;
|
||||
return false;
|
||||
}
|
||||
|
||||
extractedPanel.panelNode = std::move(panelNode);
|
||||
extractedPanel.sessionState = *sessionIt;
|
||||
session.panelStates.erase(sessionIt);
|
||||
ReorderSessionStatesToMatchWorkspace(workspace, session);
|
||||
NormalizeActivePanelAfterMutation(workspace, session, workspace.activePanelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildUIEditorDetachedWorkspaceFromExtractedPanel(
|
||||
std::string rootNodeId,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel) {
|
||||
if (rootNodeId.empty()) {
|
||||
rootNodeId = "detached-workspace-root";
|
||||
}
|
||||
|
||||
const std::string panelId = extractedPanel.panelNode.panel.panelId;
|
||||
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
workspace.root.nodeId = std::move(rootNodeId);
|
||||
workspace.root.selectedTabIndex = 0u;
|
||||
workspace.root.children.push_back(std::move(extractedPanel.panelNode));
|
||||
workspace.activePanelId = panelId;
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSession BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel) {
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates.push_back(std::move(extractedPanel.sessionState));
|
||||
return session;
|
||||
}
|
||||
|
||||
bool TryInsertExtractedUIEditorWorkspacePanelToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
if (extractedPanel.panelNode.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
extractedPanel.sessionState.panelId != extractedPanel.panelNode.panel.panelId ||
|
||||
extractedPanel.sessionState.panelId.empty() ||
|
||||
FindUIEditorPanelSessionState(session, extractedPanel.sessionState.panelId) != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
session.panelStates.push_back(extractedPanel.sessionState);
|
||||
|
||||
if (!TryInsertUIEditorWorkspacePanelNodeToStack(
|
||||
workspace,
|
||||
session,
|
||||
std::move(extractedPanel.panelNode),
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
workspace = workspaceBefore;
|
||||
session = sessionBefore;
|
||||
return false;
|
||||
}
|
||||
|
||||
ReorderSessionStatesToMatchWorkspace(workspace, session);
|
||||
NormalizeActivePanelAfterMutation(workspace, session, extractedPanel.sessionState.panelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryDockExtractedUIEditorWorkspacePanelRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
if (extractedPanel.panelNode.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
extractedPanel.sessionState.panelId != extractedPanel.panelNode.panel.panelId ||
|
||||
extractedPanel.sessionState.panelId.empty() ||
|
||||
FindUIEditorPanelSessionState(session, extractedPanel.sessionState.panelId) != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
session.panelStates.push_back(extractedPanel.sessionState);
|
||||
|
||||
if (!TryDockUIEditorWorkspacePanelNodeRelative(
|
||||
workspace,
|
||||
session,
|
||||
std::move(extractedPanel.panelNode),
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
workspace = workspaceBefore;
|
||||
session = sessionBefore;
|
||||
return false;
|
||||
}
|
||||
|
||||
ReorderSessionStatesToMatchWorkspace(workspace, session);
|
||||
NormalizeActivePanelAfterMutation(workspace, session, extractedPanel.sessionState.panelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
@@ -61,6 +61,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||
test_ui_editor_workspace_layout_persistence.cpp
|
||||
test_ui_editor_workspace_model.cpp
|
||||
test_ui_editor_workspace_session.cpp
|
||||
test_ui_editor_window_workspace_controller.cpp
|
||||
)
|
||||
|
||||
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
|
||||
@@ -88,7 +89,6 @@ if(MSVC)
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
|
||||
VS_GLOBAL_UseMultiToolTask "false"
|
||||
)
|
||||
set_property(TARGET editor_ui_tests PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
|
||||
@@ -352,6 +352,57 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
|
||||
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, ReleasingActiveTabDragOutsideDockHostRequestsDetach) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildThreeDocumentWorkspace());
|
||||
UIEditorDockHostInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{});
|
||||
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
|
||||
ASSERT_NE(documentStack, nullptr);
|
||||
const UIRect draggedTabRect = documentStack->tabStripLayout.tabHeaderRects[2];
|
||||
const UIPoint draggedTabCenter = RectCenter(draggedTabRect);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(draggedTabCenter.x, draggedTabCenter.y) });
|
||||
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::Tab);
|
||||
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-c");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerDown(draggedTabCenter.x, draggedTabCenter.y) });
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerMove(860.0f, draggedTabCenter.y) });
|
||||
EXPECT_EQ(state.activeTabDragPanelId, "doc-c");
|
||||
|
||||
frame = UpdateUIEditorDockHostInteraction(
|
||||
state,
|
||||
controller,
|
||||
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
|
||||
{ MakePointerUp(860.0f, draggedTabCenter.y) });
|
||||
EXPECT_TRUE(frame.result.detachRequested);
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.detachedNodeId, "document-tabs");
|
||||
EXPECT_EQ(frame.result.detachedPanelId, "doc-c");
|
||||
EXPECT_TRUE(state.activeTabDragNodeId.empty());
|
||||
EXPECT_TRUE(state.activeTabDragPanelId.empty());
|
||||
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
|
||||
}
|
||||
|
||||
TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationThroughTabStripInteraction) {
|
||||
auto controller =
|
||||
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Shell/UIEditorWindowWorkspaceController.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Editor::BuildDefaultUIEditorWindowWorkspaceController;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
|
||||
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
|
||||
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
|
||||
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
|
||||
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
|
||||
using XCEngine::UI::Editor::FindUIEditorWindowWorkspaceState;
|
||||
using XCEngine::UI::Editor::UIEditorPanelRegistry;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceController;
|
||||
using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
|
||||
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
|
||||
|
||||
UIEditorPanelRegistry BuildPanelRegistry() {
|
||||
UIEditorPanelRegistry registry = {};
|
||||
registry.panels = {
|
||||
{ "doc-a", "Document A", {}, true, true, true },
|
||||
{ "doc-b", "Document B", {}, true, true, true },
|
||||
{ "inspector", "Inspector", {}, true, true, true }
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildWorkspace() {
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root = BuildUIEditorWorkspaceSplit(
|
||||
"root-split",
|
||||
UIEditorWorkspaceSplitAxis::Horizontal,
|
||||
0.7f,
|
||||
BuildUIEditorWorkspaceTabStack(
|
||||
"document-tabs",
|
||||
{
|
||||
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
|
||||
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
|
||||
},
|
||||
0u),
|
||||
BuildUIEditorWorkspaceSingleTabStack(
|
||||
"inspector-panel",
|
||||
"inspector",
|
||||
"Inspector",
|
||||
true));
|
||||
workspace.activePanelId = "doc-a";
|
||||
return workspace;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const XCEngine::UI::Editor::UIEditorWorkspaceSession& session) {
|
||||
const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
std::vector<std::string> ids = {};
|
||||
ids.reserve(panels.size());
|
||||
for (const auto& panel : panels) {
|
||||
ids.push_back(panel.panelId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DetachPanelCreatesNewDetachedWindowAndMovesSessionState) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
|
||||
const auto result = controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window");
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.targetWindowId, "doc-b-window");
|
||||
EXPECT_EQ(result.activeWindowId, "doc-b-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 2u);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
EXPECT_FALSE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
EXPECT_EQ(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
|
||||
const auto* detachedWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window");
|
||||
ASSERT_NE(detachedWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(detachedWindow->workspace, "doc-b"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(detachedWindow->session, "doc-b"), nullptr);
|
||||
EXPECT_EQ(detachedWindow->workspace.activePanelId, "doc-b");
|
||||
|
||||
const auto mainVisibleIds =
|
||||
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
|
||||
ASSERT_EQ(mainVisibleIds.size(), 2u);
|
||||
EXPECT_EQ(mainVisibleIds[0], "doc-a");
|
||||
EXPECT_EQ(mainVisibleIds[1], "inspector");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, MovingSinglePanelDetachedWindowBackToMainClosesSourceWindow) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.MovePanelToStack(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
1u);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activeWindowId, "main-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 1u);
|
||||
EXPECT_EQ(result.windowIds[0], "main-window");
|
||||
|
||||
EXPECT_EQ(
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window"),
|
||||
nullptr);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-b");
|
||||
}
|
||||
|
||||
TEST(UIEditorWindowWorkspaceControllerTest, DockingDetachedPanelIntoMainWindowAlsoClosesSourceWindow) {
|
||||
UIEditorWindowWorkspaceController controller =
|
||||
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
|
||||
ASSERT_EQ(
|
||||
controller.DetachPanelToNewWindow(
|
||||
"main-window",
|
||||
"document-tabs",
|
||||
"doc-b",
|
||||
"doc-b-window").status,
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
|
||||
const auto result = controller.DockPanelRelative(
|
||||
"doc-b-window",
|
||||
"doc-b-window-root",
|
||||
"doc-b",
|
||||
"main-window",
|
||||
"inspector-panel",
|
||||
UIEditorWorkspaceDockPlacement::Left,
|
||||
0.4f);
|
||||
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
|
||||
EXPECT_EQ(result.activeWindowId, "main-window");
|
||||
ASSERT_EQ(result.windowIds.size(), 1u);
|
||||
|
||||
const auto* mainWindow =
|
||||
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
|
||||
ASSERT_NE(mainWindow, nullptr);
|
||||
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
|
||||
ASSERT_NE(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
|
||||
|
||||
const auto visibleIds =
|
||||
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
|
||||
ASSERT_EQ(visibleIds.size(), 3u);
|
||||
EXPECT_EQ(visibleIds[0], "doc-a");
|
||||
EXPECT_EQ(visibleIds[1], "doc-b");
|
||||
EXPECT_EQ(visibleIds[2], "inspector");
|
||||
}
|
||||
Reference in New Issue
Block a user