From 2abe0bbbd478bf3d549a88ef07c7ddf285cacf73 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 01:02:57 +0800 Subject: [PATCH] docs: sync project browser docs --- .../Editor/Core/EditorConsoleSink/Clear.md | 44 +++ .../Core/EditorConsoleSink/Constructor.md | 35 +++ .../Core/EditorConsoleSink/Destructor.md | 39 +++ .../EditorConsoleSink/EditorConsoleRecord.md | 45 +++ .../EditorConsoleSink/EditorConsoleSink.md | 94 ++++-- .../Editor/Core/EditorConsoleSink/Flush.md | 30 ++ .../Core/EditorConsoleSink/GetInstance.md | 47 +++ .../Editor/Core/EditorConsoleSink/GetLogs.md | 44 +++ .../Core/EditorConsoleSink/GetRecords.md | 41 +++ .../Core/EditorConsoleSink/GetRevision.md | 40 +++ .../Editor/Core/EditorConsoleSink/Log.md | 45 +++ .../Core/EditorConsoleSink/SetCallback.md | 46 +++ .../Current-Items-And-Selection.md | 58 ++++ .../Core/IProjectManager/File-Operations.md | 54 ++++ .../Core/IProjectManager/IProjectManager.md | 60 +++- .../Initialization-And-Refresh.md | 45 +++ .../IProjectManager/Navigation-And-Path.md | 54 ++++ .../ProjectRootResolver.md | 68 +++++ docs/api/XCEngine/Editor/Managers/Managers.md | 2 +- .../Managers/ProjectManager/CreateFolder.md | 40 +++ .../Managers/ProjectManager/DeleteItem.md | 50 +++ .../Managers/ProjectManager/Initialize.md | 41 +++ .../Managers/ProjectManager/MoveItem.md | 49 +++ .../Managers/ProjectManager/ProjectManager.md | 202 +++++++++++-- .../ProjectManager/RefreshCurrentFolder.md | 38 +++ .../Managers/ProjectManager/RenameItem.md | 66 ++++ .../Editor/panels/ProjectPanel/Constructor.md | 45 +++ .../Editor/panels/ProjectPanel/Initialize.md | 41 +++ .../panels/ProjectPanel/ProjectPanel.md | 284 +++++++++++++----- .../Editor/panels/ProjectPanel/Render.md | 94 ++++++ 30 files changed, 1725 insertions(+), 116 deletions(-) create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/Clear.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/Constructor.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/Destructor.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleRecord.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/Flush.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetInstance.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetLogs.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRecords.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRevision.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/Log.md create mode 100644 docs/api/XCEngine/Editor/Core/EditorConsoleSink/SetCallback.md create mode 100644 docs/api/XCEngine/Editor/Core/IProjectManager/Current-Items-And-Selection.md create mode 100644 docs/api/XCEngine/Editor/Core/IProjectManager/File-Operations.md create mode 100644 docs/api/XCEngine/Editor/Core/IProjectManager/Initialization-And-Refresh.md create mode 100644 docs/api/XCEngine/Editor/Core/IProjectManager/Navigation-And-Path.md create mode 100644 docs/api/XCEngine/Editor/Core/ProjectRootResolver/ProjectRootResolver.md create mode 100644 docs/api/XCEngine/Editor/Managers/ProjectManager/CreateFolder.md create mode 100644 docs/api/XCEngine/Editor/Managers/ProjectManager/DeleteItem.md create mode 100644 docs/api/XCEngine/Editor/Managers/ProjectManager/Initialize.md create mode 100644 docs/api/XCEngine/Editor/Managers/ProjectManager/MoveItem.md create mode 100644 docs/api/XCEngine/Editor/Managers/ProjectManager/RefreshCurrentFolder.md create mode 100644 docs/api/XCEngine/Editor/Managers/ProjectManager/RenameItem.md create mode 100644 docs/api/XCEngine/Editor/panels/ProjectPanel/Constructor.md create mode 100644 docs/api/XCEngine/Editor/panels/ProjectPanel/Initialize.md create mode 100644 docs/api/XCEngine/Editor/panels/ProjectPanel/Render.md diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Clear.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Clear.md new file mode 100644 index 00000000..53c9f418 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Clear.md @@ -0,0 +1,44 @@ +# EditorConsoleSink::Clear + +清空当前控制台缓冲区。 + +```cpp +void Clear(); +``` + +## 当前行为 + +当前实现分两种情况: + +### 缓冲区已经为空 + +- 直接返回 +- 不递增 `revision` +- 不触发回调 + +### 缓冲区非空 + +1. 在锁内清空 `m_logs` +2. 递增 `m_revision` +3. 拷贝当前回调 +4. 解锁后执行回调 + +## 当前不会重置的状态 + +`Clear()` 不会重置: + +- `m_nextSerial` +- 已注册回调 + +所以清空之后再写入新日志时,新的 `serial` 会继续从上次编号往后增长。 + +## 返回值 + +- 无。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [GetRevision](GetRevision.md) +- [SetCallback](SetCallback.md) +- [Log](Log.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Constructor.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Constructor.md new file mode 100644 index 00000000..2a4f2850 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Constructor.md @@ -0,0 +1,35 @@ +# EditorConsoleSink::Constructor + +构造一个编辑器控制台日志 sink,并把它登记为当前活动实例。 + +```cpp +EditorConsoleSink(); +``` + +## 当前行为 + +当前构造函数本身没有复杂初始化逻辑,它只做一件关键的事: + +- 把静态指针 `s_instance` 直接设为 `this` + +对象其余状态使用头文件里的默认成员初始化: + +- `m_logs` 为空 +- `m_callback` 为空 +- `m_nextSerial == 1` +- `m_revision == 0` + +## 设计含义 + +这意味着“最后构造出来的实例”会覆盖之前登记的活动实例。 +当前实现没有实例栈,也没有显式注册/反注册 API。 + +## 返回值 + +- 无。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [GetInstance](GetInstance.md) +- [Destructor](Destructor.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Destructor.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Destructor.md new file mode 100644 index 00000000..7eb0a034 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Destructor.md @@ -0,0 +1,39 @@ +# EditorConsoleSink::Destructor + +销毁控制台 sink,并在必要时撤销活动实例注册。 + +```cpp +~EditorConsoleSink() override; +``` + +## 当前行为 + +析构函数会检查: + +```cpp +s_instance == this +``` + +只有当当前对象仍然是活动实例时,才会把 `s_instance` 置空。 +如果活动实例已经被其他后来构造的对象覆盖,析构当前对象不会再改全局指针。 + +## 析构后的可见行为 + +一旦活动实例被清空,后续 [GetInstance](GetInstance.md) 会直接返回 `nullptr`。 +如果当前对象在析构前已经被后来构造的实例覆盖,则析构不会影响那个更新的活动实例。 + +## 当前不做的事情 + +- 不会自动 `Flush()` +- 不会转移现有日志 +- 不会通知回调“实例已销毁” + +## 返回值 + +- 无。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [GetInstance](GetInstance.md) +- [Constructor](Constructor.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleRecord.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleRecord.md new file mode 100644 index 00000000..eeee46a2 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleRecord.md @@ -0,0 +1,45 @@ +# EditorConsoleRecord + +**命名空间**: `XCEngine::Debug` + +**类型**: `struct` + +**源文件**: `editor/src/Core/EditorConsoleSink.h` + +**描述**: `EditorConsoleSink` 内部使用的控制台记录快照,给每条日志附加一个单调递增的 `serial`。 + +## 概述 + +`EditorConsoleRecord` 是 `LogEntry` 的 Editor 侧包装层。 +它解决的问题不是“如何描述一条日志”,而是“如何在内存缓冲区里稳定地区分和追踪每一条日志”。 + +相比裸 `LogEntry`,它新增了一个 `serial` 字段,用来表达: + +- 这条日志在当前 sink 实例中的追加顺序 +- Console 列表选中项的稳定身份 +- `Error Pause` 扫描“上次检查之后新增日志”的边界 + +## 字段 + +| 字段 | 说明 | +|------|------| +| `serial` | 当前 sink 实例内单调递增的记录编号。 | +| `entry` | 原始日志条目副本。 | + +## serial 语义 + +- `serial` 从 `1` 开始递增。 +- [Clear](Clear.md) 不会把它重置回 `1`。 +- 它只在当前进程、当前 sink 生命周期内有意义。 +- 它不是时间戳,也不是可持久化的稳定 ID。 + +## 当前使用位置 + +- [ConsolePanel](../../panels/ConsolePanel/ConsolePanel.md) 用它保持选择、做键盘导航和双击定位。 +- `Error Pause` 流程用它记录“最近已经扫描到哪一条”。 + +## 相关文档 + +- [EditorConsoleSink](EditorConsoleSink.md) +- [Log](Log.md) +- [GetRecords](GetRecords.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleSink.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleSink.md index 7d73dd90..23584edd 100644 --- a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleSink.md +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/EditorConsoleSink.md @@ -6,37 +6,91 @@ **源文件**: `editor/src/Core/EditorConsoleSink.h` -**描述**: 编辑器专用日志 sink,把引擎日志桥接到 Console 面板可读取的内存缓冲区。 +**描述**: 编辑器专用日志 sink,把 `Logger` 的输出桥接成带 `serial` / `revision` 的内存快照,供 `ConsolePanel` 增量消费。 ## 概述 -`EditorConsoleSink` 虽然位于 `editor/src/Core`,但命名空间属于 `XCEngine::Debug`。 -它的定位很明确:作为 `Logger` 的一个输出目标,把日志保存在内存中,供 Console 面板显示。 +`EditorConsoleSink` 虽然位于 `editor/src/Core`,但命名空间仍属于 `XCEngine::Debug`。 +它不是单纯“把日志塞进数组”的轻量容器,而是当前 Editor Console 数据流的中心桥接层,负责三件事: -## 当前实现 +1. 作为 `ILogSink` 接收引擎日志。 +2. 把日志保存成带 [EditorConsoleRecord](EditorConsoleRecord.md) 的内存快照。 +3. 通过 `serial`、`revision` 和变更回调让 [ConsolePanel](../../panels/ConsolePanel/ConsolePanel.md) 可以做增量刷新、错误暂停扫描和选择保持。 -- 继承自 `ILogSink` -- 通过 `std::mutex` 保护 `m_logs` -- `Log()` 会把新日志追加到数组尾部 -- 最大缓存条数固定为 `1000` -- 超限时直接丢弃最早的一条日志 -- `GetLogs()` 返回日志数组的拷贝 -- `GetInstance()` 在没有显式注册实例时会返回一个静态 fallback 实例 +## 当前数据模型 -## 设计说明 +### 日志记录 -这是一个典型的“工具层 sink”。 -引擎日志系统不应该知道编辑器面板怎么画 UI,但编辑器又确实需要消费同一份日志流。把桥接能力做成一个 `ILogSink` 子类,是最干净的集成方式。 +内部缓冲区 `m_logs` 保存的是 `EditorConsoleRecord`,而不是裸 `LogEntry`: -## 当前限制 +- `serial` 是 sink 内单调递增的记录编号。 +- `entry` 是完整日志条目副本。 -- 当前只保留最近 1000 条日志 -- `GetLogs()` 会复制整个缓冲区 -- `SetCallback()` 没有更复杂的线程调度机制,回调触发策略非常轻量 +`ConsolePanel` 当前依赖这个 `serial` 做: + +- 新增错误扫描边界 +- 列表选中保持 +- 折叠/搜索后重新定位选中项 + +### revision + +`m_revision` 是比 `serial` 更粗粒度的“内容变更版本号”: + +- 每次 [Log](Log.md) 成功追加日志时加一 +- 每次 [Clear](Clear.md) 真正清空非空缓冲区时加一 +- [SetCallback](SetCallback.md) 和 [Flush](Flush.md) 不会改它 + +它的用途是让 UI 先判断“有没有变化”,再决定是否重建可见行。 + +## 当前生命周期 + +- [Constructor](Constructor.md) 会把当前对象登记为全局活动实例。 +- [Destructor](Destructor.md) 只在自己仍是活动实例时清掉全局指针。 +- [GetInstance](GetInstance.md) 只返回当前登记的活动实例;若当前没有活动 sink,则返回 `nullptr`。 + +这意味着当前接口层面上 `GetInstance()` 可能返回空指针。调用方需要像 [ConsolePanel](../../panels/ConsolePanel/ConsolePanel.md) 那样先判空,再进入日志读取与 UI 刷新流程。 + +## 线程语义 + +- `m_mutex` 保护 `m_logs`、`m_callback`、`m_nextSerial` 和 `m_revision`。 +- [GetLogs](GetLogs.md)、[GetRecords](GetRecords.md)、[GetRevision](GetRevision.md) 都返回锁内拍下的快照,不暴露内部引用。 +- [Log](Log.md) 和 [Clear](Clear.md) 会先在锁内取出当前回调,再在解锁后执行回调。 + +最后一点很关键: +当前回调不是调度到 UI 线程,而是谁触发 `Log()` / `Clear()`,谁就在自己的调用线程上同步执行回调。 + +## 当前实现边界 + +- 最大缓存条数固定为 `1000`,超限时直接丢弃最早一条。 +- `GetLogs()` / `GetRecords()` 都会复制整份缓冲区,不是零拷贝视图。 +- [Flush](Flush.md) 当前是空实现,只是满足 `ILogSink` 契约。 +- [SetCallback](SetCallback.md) 只保存一个回调,后设置的会覆盖先前的。 +- [Clear](Clear.md) 不会重置 `m_nextSerial`,所以清空后新日志仍继续增长编号。 + +## 公开方法 + +| 方法 | 说明 | +|------|------| +| [GetInstance](GetInstance.md) | 获取当前活动 sink;若没有已注册实例则返回 `nullptr`。 | +| [Constructor](Constructor.md) | 构造一个可注册为活动实例的编辑器日志 sink。 | +| [Destructor](Destructor.md) | 在销毁时撤销活动实例注册。 | +| [Log](Log.md) | 追加一条日志并在需要时通知观察者。 | +| [Flush](Flush.md) | 满足 `ILogSink` 接口的空刷新函数。 | +| [GetLogs](GetLogs.md) | 返回不含 `serial` 的 `LogEntry` 快照。 | +| [GetRecords](GetRecords.md) | 返回带 `serial` 的完整控制台记录快照。 | +| [GetRevision](GetRevision.md) | 返回当前缓冲区的变更版本号。 | +| [Clear](Clear.md) | 清空当前缓冲区并发布一次内容变化。 | +| [SetCallback](SetCallback.md) | 注册或替换一条内容变更回调。 | + +## 相关类型 + +- [EditorConsoleRecord](EditorConsoleRecord.md) +- [LogEntry](../../../Debug/LogEntry/LogEntry.md) ## 相关文档 - [Core](../Core.md) -- [ConsoleActionRouter](../../Actions/ConsoleActionRouter/ConsoleActionRouter.md) - [ConsolePanel](../../panels/ConsolePanel/ConsolePanel.md) -- [Debug::Logger](../../../Debug/Logger/Logger.md) +- [ConsoleFilterState](../../UI/ConsoleFilterState/ConsoleFilterState.md) +- [ILogSink](../../../Debug/ILogSink/ILogSink.md) +- [Logger](../../../Debug/Logger/Logger.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Flush.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Flush.md new file mode 100644 index 00000000..8e2744b4 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Flush.md @@ -0,0 +1,30 @@ +# EditorConsoleSink::Flush + +满足 `ILogSink` 接口的刷新函数。 + +```cpp +void Flush() override; +``` + +## 当前行为 + +当前实现为空函数,不执行任何操作。 + +## 原因 + +`EditorConsoleSink` 只维护内存缓冲区,没有: + +- 文件句柄 +- socket +- 延迟写盘缓存 + +所以它不需要像文件 sink 那样做显式落地。 + +## 返回值 + +- 无。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [ILogSink](../../../Debug/ILogSink/ILogSink.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetInstance.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetInstance.md new file mode 100644 index 00000000..3407adbe --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetInstance.md @@ -0,0 +1,47 @@ +# EditorConsoleSink::GetInstance + +获取当前活动的编辑器控制台 sink。 + +```cpp +static EditorConsoleSink* GetInstance(); +``` + +## 当前行为 + +当前实现没有兜底实例,也不会在这里隐式构造对象。它只做一件事: + +1. 直接返回静态指针 `s_instance` 的当前值。 + +因此: + +- 若已有活动 sink 被构造并登记,返回那个实例。 +- 若当前没有活动 sink,直接返回 `nullptr`。 + +## 实例来源 + +活动实例的登记与撤销完全由生命周期驱动: + +- [Constructor](Constructor.md) 会把 `s_instance` 设为 `this` +- [Destructor](Destructor.md) 会在 `s_instance == this` 时把它清空 + +该方法本身不负责: + +- 创建 fallback sink +- 维持实例栈 +- 区分“正式实例”和“临时实例” + +## 注意事项 + +- 返回的是原始指针,不表达所有权。 +- 调用方必须处理 `nullptr`,不能假设 Editor 全程都已注册控制台 sink。 +- 如果较晚构造的新实例覆盖了旧实例,后续调用会直接观察到新的 `s_instance`。 + +## 返回值 + +- 当前活动实例;如果没有已注册实例,则返回 `nullptr`。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [Constructor](Constructor.md) +- [Destructor](Destructor.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetLogs.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetLogs.md new file mode 100644 index 00000000..59f27df6 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetLogs.md @@ -0,0 +1,44 @@ +# EditorConsoleSink::GetLogs + +返回不含 `serial` 的日志条目快照。 + +```cpp +std::vector GetLogs() const; +``` + +## 当前行为 + +当前实现会: + +1. 在锁内读取 `m_logs` +2. 新建一个 `std::vector` +3. 把每条 `EditorConsoleRecord::entry` 复制进去 +4. 返回这份副本 + +## 与 GetRecords() 的区别 + +- [GetLogs](GetLogs.md) 只返回 `LogEntry` +- [GetRecords](GetRecords.md) 保留 `serial` + +如果调用方需要做: + +- 增量刷新 +- 选中保持 +- 错误扫描边界 + +应优先使用 `GetRecords()`。 + +## 成本与限制 + +- 返回值是整份副本,不是视图。 +- 当前调用一次就会复制当前全部缓冲区。 + +## 返回值 + +- 当前日志缓冲区的 `LogEntry` 副本数组。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [GetRecords](GetRecords.md) +- [Log](Log.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRecords.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRecords.md new file mode 100644 index 00000000..2305880e --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRecords.md @@ -0,0 +1,41 @@ +# EditorConsoleSink::GetRecords + +返回带 `serial` 的控制台记录快照。 + +```cpp +std::vector GetRecords() const; +``` + +## 当前行为 + +当前实现非常直接: + +1. 在锁内复制 `m_logs` +2. 返回整份 `std::vector` 副本 + +这也是 [ConsolePanel](../../panels/ConsolePanel/ConsolePanel.md) 当前的主要读取接口。 + +## 为什么需要它 + +相比 [GetLogs](GetLogs.md),这里保留了 `serial`。 +这让调用方可以知道: + +- 哪些日志是“新追加”的 +- 当前选中项在过滤/折叠后对应哪条记录 +- `Error Pause` 应该从哪条记录之后开始扫描 + +## 快照语义 + +- 返回的是调用时刻的拷贝。 +- 后续新的 `Log()` 或 `Clear()` 不会回写到已经拿到的返回值。 + +## 返回值 + +- 当前控制台缓冲区的完整 `EditorConsoleRecord` 副本数组。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [EditorConsoleRecord](EditorConsoleRecord.md) +- [GetLogs](GetLogs.md) +- [GetRevision](GetRevision.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRevision.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRevision.md new file mode 100644 index 00000000..ee449471 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/GetRevision.md @@ -0,0 +1,40 @@ +# EditorConsoleSink::GetRevision + +返回当前控制台缓冲区的变更版本号。 + +```cpp +uint64_t GetRevision() const; +``` + +## 当前行为 + +该方法会在锁内读取并返回 `m_revision`。 + +## revision 递增规则 + +当前实现里,`revision` 只在两类内容变化时递增: + +- [Log](Log.md) 追加一条日志 +- [Clear](Clear.md) 真正清空一个非空缓冲区 + +以下操作不会改它: + +- [SetCallback](SetCallback.md) +- [Flush](Flush.md) +- 对空缓冲区调用 [Clear](Clear.md) + +## 典型用途 + +UI 层通常先比较 `revision`,只有检测到变化时才重建过滤结果或决定是否滚动到底部。 +这比每帧都深比较整份日志数组更便宜。 + +## 返回值 + +- 当前缓冲区版本号。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [Log](Log.md) +- [Clear](Clear.md) +- [GetRecords](GetRecords.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Log.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Log.md new file mode 100644 index 00000000..21257ca4 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/Log.md @@ -0,0 +1,45 @@ +# EditorConsoleSink::Log + +追加一条日志到控制台缓冲区,并在需要时通知观察者。 + +```cpp +void Log(const LogEntry& entry) override; +``` + +## 参数 + +- `entry` - 待写入控制台缓冲区的日志条目。 + +## 当前行为 + +当前实现流程是: + +1. 进入互斥区。 +2. 如果缓冲区已达到 `MAX_LOGS == 1000`,先删除最早的一条。 +3. 以当前 `m_nextSerial` 构造一条 [EditorConsoleRecord](EditorConsoleRecord.md)。 +4. 递增 `m_nextSerial`。 +5. 递增 `m_revision`。 +6. 拷贝当前回调到局部变量。 +7. 离开互斥区后执行回调。 + +## serial 与裁剪语义 + +- 新日志总是追加到尾部。 +- 超限裁剪只丢弃最老记录,不影响新记录的 `serial` 递增。 +- 即使缓冲区被多次清空,后续日志的 `serial` 仍然继续增长。 + +## 回调线程语义 + +回调不是异步投递,而是由 `Log()` 的调用线程同步执行。 +当前实现之所以先复制回调、再解锁后调用,是为了避免回调内部再次访问 sink 时发生重入锁问题。 + +## 返回值 + +- 无。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [EditorConsoleRecord](EditorConsoleRecord.md) +- [GetRevision](GetRevision.md) +- [SetCallback](SetCallback.md) diff --git a/docs/api/XCEngine/Editor/Core/EditorConsoleSink/SetCallback.md b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/SetCallback.md new file mode 100644 index 00000000..c2059d4b --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/EditorConsoleSink/SetCallback.md @@ -0,0 +1,46 @@ +# EditorConsoleSink::SetCallback + +注册或替换一条“控制台内容变化”回调。 + +```cpp +void SetCallback(std::function callback); +``` + +## 参数 + +- `callback` - 新的通知回调;可以为空。 + +## 当前行为 + +当前实现只是在锁内执行: + +- `m_callback = std::move(callback)` + +它不会: + +- 立即触发新回调 +- 保留旧回调形成链式通知 +- 做线程切换或延迟调度 + +## 触发时机 + +新回调会在以下操作后被调用: + +- [Log](Log.md) 成功追加日志 +- [Clear](Clear.md) 真正清空非空缓冲区 + +并且都是在对应调用线程上、解锁之后同步执行。 + +## 使用建议 + +如果回调里要刷新 UI 或触发更上层逻辑,调用方应自己保证线程上下文安全,不要假设这里已经切到主线程。 + +## 返回值 + +- 无。 + +## 相关文档 + +- [返回类型总览](EditorConsoleSink.md) +- [Log](Log.md) +- [Clear](Clear.md) diff --git a/docs/api/XCEngine/Editor/Core/IProjectManager/Current-Items-And-Selection.md b/docs/api/XCEngine/Editor/Core/IProjectManager/Current-Items-And-Selection.md new file mode 100644 index 00000000..0a415b4a --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IProjectManager/Current-Items-And-Selection.md @@ -0,0 +1,58 @@ +# IProjectManager / Current Items And Selection + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface-group` + +**源文件**: `editor/src/Core/IProjectManager.h` + +## 相关签名 + +```cpp +virtual const std::vector& GetCurrentItems() const = 0; +virtual AssetItemPtr GetRootFolder() const = 0; +virtual AssetItemPtr GetCurrentFolder() const = 0; + +virtual AssetItemPtr GetSelectedItem() const = 0; +virtual const std::string& GetSelectedItemPath() const = 0; +virtual int GetSelectedIndex() const = 0; +virtual void SetSelectedIndex(int index) = 0; +virtual void SetSelectedItem(const AssetItemPtr& item) = 0; +virtual void ClearSelection() = 0; +virtual int FindCurrentItemIndex(const std::string& fullPath) const = 0; +``` + +## 作用 + +这一组接口定义的是“当前正在浏览的目录里有什么,以及当前选中了什么”。 + +调用方通常用它们驱动: + +- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) 的目录内容绘制 +- 资源卡片选中高亮 +- 上下文菜单目标同步 +- 基于当前目录的资源创建、删除、重命名和拖放移动 + +## 语义说明 + +- `GetCurrentItems()` 返回当前目录直接子项,不是全项目递归结果。 +- `GetRootFolder()` 与 `GetCurrentFolder()` 都是 `AssetItem` 视图节点,而不是磁盘句柄。 +- `GetSelectedItemPath()` 提供稳定的路径标识,便于刷新后重新解析当前选择。 +- `GetSelectedIndex()` / `SetSelectedIndex()` 以“当前目录列表索引”为坐标,不跨目录。 +- `FindCurrentItemIndex(fullPath)` 只在当前目录中找,不做全树搜索。 + +## 当前默认实现 + +在 [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) 中: + +- 选择状态按 `m_selectedItemPath` 保存,而不是长期持有旧 `AssetItemPtr` +- `GetSelectedItem()` 会在当前目录里按路径重新解析 +- `SetSelectedIndex()` 越界时会清空选择 +- `RefreshCurrentFolder()` 后若旧路径仍在当前目录内,选择可被恢复 + +## 相关文档 + +- [IProjectManager](IProjectManager.md) +- [Navigation And Path](Navigation-And-Path.md) +- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) +- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) diff --git a/docs/api/XCEngine/Editor/Core/IProjectManager/File-Operations.md b/docs/api/XCEngine/Editor/Core/IProjectManager/File-Operations.md new file mode 100644 index 00000000..abd2d8b8 --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IProjectManager/File-Operations.md @@ -0,0 +1,54 @@ +# IProjectManager / File Operations + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface-group` + +**源文件**: `editor/src/Core/IProjectManager.h` + +## 相关签名 + +```cpp +virtual AssetItemPtr CreateFolder(const std::string& name) = 0; +virtual bool DeleteItem(const std::string& fullPath) = 0; +virtual bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath) = 0; +virtual bool RenameItem(const std::string& sourceFullPath, const std::string& newName) = 0; + +virtual const std::string& GetProjectPath() const = 0; +``` + +## 作用 + +这一组接口定义的是项目浏览器层面的基础资源改动能力。 + +它们负责: + +- 在当前目录下创建文件夹 +- 删除某个资源或目录 +- 把资源移动到目标目录 +- 重命名资源或目录 +- 暴露项目根路径,供命令层生成相对路径、项目描述文件和 fallback 场景路径 + +## 参数与返回值 + +- `CreateFolder(name)` 返回新建目录对应的 `AssetItemPtr`,失败时返回 `nullptr` +- `DeleteItem(...)` / `MoveItem(...)` / `RenameItem(...)` 以布尔值表示成功或失败 +- `MoveItem(...)` 的目标参数是“目标目录完整路径”,不是目标文件完整路径 +- `GetProjectPath()` 返回项目根目录;它和 `GetCurrentPath()` 的含义不同 + +## 当前默认实现 + +在 [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) 中,这组操作还承担了若干实际约束: + +- 名称会先 trim,并拒绝 Windows 非法文件名 +- 所有改动必须位于当前项目 `Assets` 根目录之内 +- 根目录本身不能被删除、移动或重命名 +- `.meta` sidecar 会在删除、移动、重命名时跟随同步 +- 完成后会刷新目录树,并尽量保持当前路径与选择一致 + +## 相关文档 + +- [IProjectManager](IProjectManager.md) +- [Initialization And Refresh](Initialization-And-Refresh.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) +- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) diff --git a/docs/api/XCEngine/Editor/Core/IProjectManager/IProjectManager.md b/docs/api/XCEngine/Editor/Core/IProjectManager/IProjectManager.md index 6ae7aa50..27332ffa 100644 --- a/docs/api/XCEngine/Editor/Core/IProjectManager/IProjectManager.md +++ b/docs/api/XCEngine/Editor/Core/IProjectManager/IProjectManager.md @@ -6,36 +6,82 @@ **源文件**: `editor/src/Core/IProjectManager.h` -**描述**: 定义项目浏览器接口,负责当前资产列表、路径导航、文件夹刷新以及基础资源文件操作。 +**描述**: 定义 editor 项目工作流的抽象接口,负责当前资产列表、路径导航、选择状态,以及项目 `Assets` 树上的基础文件操作契约。 ## 概述 -`IProjectManager` 面向的是编辑器 Project 面板这一类工作流。 +`IProjectManager` 面向的不只是 `ProjectPanel` 的目录浏览,它还是 editor 侧项目资源模型的抽象接口。 -它的接口组合非常清晰: +按当前头文件,它的接口可以分成五组: - 当前目录的资产项列表 -- 当前选中项索引 +- 当前选中项与选中路径 - 路径导航 -- 项目根路径 -- 文件夹刷新/创建/删除/移动 +- 项目根路径与目录刷新 +- 创建 / 删除 / 移动 / 重命名 + +因此它是当前这条链路里的中间抽象层: + +```text +MainMenuActionRouter / ProjectPanel +-> ProjectCommands +-> IProjectManager +-> ProjectManager +``` + +菜单层和面板层都不直接操作文件系统细节,而是通过这个接口进入项目模型与文件操作。 ## 核心接口 | 方法 | 作用 | |------|------| | `GetCurrentItems()` | 获取当前目录条目。 | +| `GetSelectedItem()` / `GetSelectedItemPath()` | 获取当前选中资源及其路径。 | | `GetSelectedIndex()` / `SetSelectedIndex()` | 管理当前选中项索引。 | +| `SetSelectedItem()` / `ClearSelection()` | 直接管理当前选择。 | | `NavigateToFolder()` / `NavigateBack()` / `NavigateToIndex()` | 处理路径导航。 | | `CanNavigateBack()` | 判断能否返回上一级。 | | `GetCurrentPath()` / `GetPathDepth()` / `GetPathName()` | 查询当前路径面包屑。 | | `Initialize()` | 初始化项目目录。 | | `RefreshCurrentFolder()` | 刷新当前目录内容。 | -| `CreateFolder()` / `DeleteItem()` / `MoveItem()` | 执行基础文件操作。 | +| `CreateFolder()` / `DeleteItem()` / `MoveItem()` / `RenameItem()` | 执行基础文件操作。 | | `GetProjectPath()` | 获取项目根目录。 | +## 边界与职责 + +`IProjectManager` 当前明确不负责: + +- 文件对话框 +- 菜单文本或快捷键 +- 资产数据库导入 / artifact 构建 +- 项目级脚本构建 +- 项目保存与项目切换策略 + +它负责的是同步项目模型与维护动作契约,也就是: + +- UI 层能看到什么 +- 基础文件操作怎么做 +- 当前项目根目录和 `Assets` 浏览树如何对外暴露 + +这也是当前接口设计上的一个取舍: + +- 资源浏览和基础文件操作都通过同步返回值表达结果 +- 更高层的项目保存、脚本构建和项目切换仍留给 [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) 与 `Application` +- 界面层如何呈现这些结果,仍由上层自己决定 + +## 设计说明 + +把 `ProjectPanel` 依赖的目录模型、导航状态和文件操作抽象成接口,有两个直接收益: + +- UI 层可以围绕 `AssetItem` 模型稳定开发,而不必直接接触文件系统细节 +- 更高层的项目命令层可以只依赖抽象,而不耦合到具体实现 + +这也是为什么 `IProjectManager` 的当前职责比较克制:它更像“项目资源浏览模型接口”,而不是完整的项目系统门面。 + ## 相关文档 - [Core](../Core.md) - [AssetItem](../AssetItem/AssetItem.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) +- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) - [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) diff --git a/docs/api/XCEngine/Editor/Core/IProjectManager/Initialization-And-Refresh.md b/docs/api/XCEngine/Editor/Core/IProjectManager/Initialization-And-Refresh.md new file mode 100644 index 00000000..0127462a --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IProjectManager/Initialization-And-Refresh.md @@ -0,0 +1,45 @@ +# IProjectManager / Initialization And Refresh + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface-group` + +**源文件**: `editor/src/Core/IProjectManager.h` + +## 相关签名 + +```cpp +virtual void Initialize(const std::string& projectPath) = 0; +virtual void RefreshCurrentFolder() = 0; +``` + +## 作用 + +这一组接口负责把 manager 绑定到某个项目根目录,并在外部文件系统状态变化后刷新当前浏览视图。 + +它们通常由以下工作流触发: + +- [ProjectPanel::Initialize](../../panels/ProjectPanel/Initialize.md) +- [ProjectCommands::SwitchProject](../../Commands/ProjectCommands/ProjectCommands.md) +- `SaveProject(...)`、`RebuildScriptAssemblies(...)` 这类可能影响项目目录状态的命令 + +## 语义说明 + +- `Initialize(projectPath)` 把 manager 切到新的项目上下文。 +- `RefreshCurrentFolder()` 重建当前项目资产树的可见部分。 +- 这两者都只负责项目浏览模型,不负责文件对话框、场景切换确认或撤销历史重置。 + +## 当前默认实现 + +在 [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) 中: + +- `Initialize(...)` 会把 `/Assets` 作为根目录,并确保最小的 `Assets/Scenes` 结构存在 +- `RefreshCurrentFolder()` 会重建目录树,同时尽量保留当前路径和当前选择 +- 若扫描或文件系统操作抛异常,默认实现倾向于保留最小可工作的空根目录,而不是把异常继续上抛 + +## 相关文档 + +- [IProjectManager](IProjectManager.md) +- [File Operations](File-Operations.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) +- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) diff --git a/docs/api/XCEngine/Editor/Core/IProjectManager/Navigation-And-Path.md b/docs/api/XCEngine/Editor/Core/IProjectManager/Navigation-And-Path.md new file mode 100644 index 00000000..caf68e4f --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/IProjectManager/Navigation-And-Path.md @@ -0,0 +1,54 @@ +# IProjectManager / Navigation And Path + +**命名空间**: `XCEngine::Editor` + +**类型**: `interface-group` + +**源文件**: `editor/src/Core/IProjectManager.h` + +## 相关签名 + +```cpp +virtual void NavigateToFolder(const AssetItemPtr& folder) = 0; +virtual void NavigateBack() = 0; +virtual void NavigateToIndex(size_t index) = 0; +virtual bool CanNavigateBack() const = 0; + +virtual std::string GetCurrentPath() const = 0; +virtual size_t GetPathDepth() const = 0; +virtual std::string GetPathName(size_t index) const = 0; +``` + +## 作用 + +这一组接口把项目浏览路径暴露成了一个显式的面包屑模型,而不只是“当前目录指针”。 + +上层通常用它们驱动: + +- 左侧目录树点击导航 +- 右侧 breadcrumb 点击回跳 +- “是否还能返回上级目录”的按钮或状态判断 + +## 路径模型 + +- `NavigateToFolder(...)` 直接跳到某个目录节点。 +- `NavigateBack()` 回退一级。 +- `NavigateToIndex(index)` 以 breadcrumb 层级为单位回跳。 +- `GetPathDepth()` / `GetPathName(index)` 提供 breadcrumb 数据源。 +- `GetCurrentPath()` 返回逻辑浏览路径,而不是磁盘绝对路径。 + +## 当前默认实现 + +在 [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) 中: + +- 路径栈由 `m_path` 维护 +- `GetCurrentPath()` 始终从 `"Assets"` 开始拼接 +- `CanNavigateBack()` 只在 `m_path.size() > 1` 时返回 `true` +- 导航发生后会清空当前资源选择,避免旧目录索引泄漏到新目录 + +## 相关文档 + +- [IProjectManager](IProjectManager.md) +- [Current Items And Selection](Current-Items-And-Selection.md) +- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) +- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) diff --git a/docs/api/XCEngine/Editor/Core/ProjectRootResolver/ProjectRootResolver.md b/docs/api/XCEngine/Editor/Core/ProjectRootResolver/ProjectRootResolver.md new file mode 100644 index 00000000..1da6283a --- /dev/null +++ b/docs/api/XCEngine/Editor/Core/ProjectRootResolver/ProjectRootResolver.md @@ -0,0 +1,68 @@ +# ProjectRootResolver + +**命名空间**: `XCEngine::Editor` + +**类型**: `header-helper` + +**源文件**: `editor/src/Core/ProjectRootResolver.h` + +**描述**: 提供编辑器启动时的项目根目录解析和工作目录切换 helper。 + +## 概述 + +`ProjectRootResolver.h` 解决的是“编辑器现在应该把哪个目录视为当前项目根”的问题。 + +当前 [Application](../../Application/Application.md) 会在启动时调用这里的 helper,尽量把项目定位到: + +- 命令行显式指定的路径 +- 当前工作区下的默认 `project/` +- 或者最后回退到工作目录 / 可执行文件目录 + +## 当前解析顺序 + +`ResolveEditorProjectRootUtf8()` 的顺序是: + +1. 读取当前工作目录与可执行目录。 +2. 先解析命令行 `--project` / `-p` 覆盖项。 +3. 向上查找工作区根目录。 +4. 若找到工作区,则优先使用 `/project`。 +5. 若仍找不到,再尝试从可执行目录向上查找工作区。 +6. 最后回退到工作目录,若工作目录为空则回退到可执行目录。 + +## 工作区与项目判定 + +内部 helper 当前使用的规则很直接: + +- `IsWorkspaceRoot(candidate)` + - 需要同时存在 `CMakeLists.txt` + - 需要有 `editor/` 与 `engine/` 目录 +- `IsEditorProjectRoot(candidate)` + - 只要存在 `Project.xcproject`,或存在 `Assets/` 目录 + +这说明它不是通用项目发现器,而是明显面向当前仓库结构的启动辅助逻辑。 + +## 默认项目策略 + +`ResolveWorkspaceDefaultProjectRoot()` 当前优先返回: + +- `/project` + +如果该目录本身已经符合项目根判定,就直接使用它;否则才尝试把工作区根本身当作项目根。 + +## 工作目录切换 + +`SetEditorWorkingDirectory(projectRootUtf8)` 会把进程当前工作目录切换到解析出来的项目根,并返回是否成功。 + +这保证了后续相对路径、资源加载和项目输出目录尽量围绕项目根工作,而不是围绕可执行文件目录。 + +## 当前实现边界 + +- 当前实现明显偏 Windows 桌面路径和当前仓库布局,不是跨平台项目定位库。 +- 命令行解析只识别 `--project` / `-p` 两种形式。 +- `FindWorkspaceRoot()` 依赖向上查找 `CMakeLists.txt + editor + engine` 组合,不适合其他目录结构。 + +## 相关文档 + +- [Core](../Core.md) +- [Application](../../Application/Application.md) +- [Win32Utf8](../../Platform/Win32Utf8/Win32Utf8.md) diff --git a/docs/api/XCEngine/Editor/Managers/Managers.md b/docs/api/XCEngine/Editor/Managers/Managers.md index 307bed71..466fe827 100644 --- a/docs/api/XCEngine/Editor/Managers/Managers.md +++ b/docs/api/XCEngine/Editor/Managers/Managers.md @@ -18,7 +18,7 @@ ## 当前实现特征 -- `ProjectManager` 更偏文件系统浏览器。 +- `ProjectManager` 更偏文件系统浏览器,但当前已经包含 `.meta` sidecar 跟随、case-only rename 和项目目录初始化等项目维护语义。 - `SceneManager` 更偏场景编辑服务与场景文件生命周期管理。 - `SelectionManager` 则是旧版单例实现,当前主链路已转向 `Core/ISelectionManager + Core/SelectionManager`。 diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/CreateFolder.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/CreateFolder.md new file mode 100644 index 00000000..df460a49 --- /dev/null +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/CreateFolder.md @@ -0,0 +1,40 @@ +# ProjectManager::CreateFolder + +```cpp +AssetItemPtr CreateFolder(const std::string& name); +``` + +## 当前行为 + +`CreateFolder(...)` 当前在“当前目录”下创建新文件夹,而不是在任意传入路径下创建。 + +它会: + +1. trim 输入名称 +2. 拒绝空名、`.`、`..` 和 Windows 非法文件名字符 +3. 在当前目录下计算唯一目标名 +4. 执行 `create_directory(...)` +5. 刷新目录树 +6. 查回新建目录对应的 `AssetItem` +7. 自动把它设为当前选中项 + +## 唯一命名规则 + +若目标目录已存在,当前会追加数字后缀: + +- `New Folder` +- `New Folder 1` +- `New Folder 2` + +这与 `tests/editor/test_action_routing.cpp` 的 `ProjectCommandsCreateFolderUsesUniqueDefaultName` 一致。 + +## 返回值语义 + +- 创建并刷新成功时,返回新目录对应的 `AssetItemPtr` +- 参数非法、目录创建失败或刷新后无法重新解析该目录时,返回 `nullptr` + +## 相关文档 + +- [ProjectManager](ProjectManager.md) +- [RefreshCurrentFolder](RefreshCurrentFolder.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/DeleteItem.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/DeleteItem.md new file mode 100644 index 00000000..b49356c9 --- /dev/null +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/DeleteItem.md @@ -0,0 +1,50 @@ +# ProjectManager::DeleteItem + +```cpp +bool DeleteItem(const std::string& fullPath); +``` + +## 当前行为 + +`DeleteItem(...)` 只允许删除当前项目 `Assets` 根目录之下的条目。 + +当前会依次校验: + +- `fullPath` 非空 +- `m_rootFolder` 已存在 +- 目标路径存在 +- 目标路径位于当前项目根目录之下 +- 目标路径不是根目录本身 + +通过后才会执行真正删除。 + +## 删除语义 + +当前删除动作包括两步: + +1. `fs::remove_all(itemPath)` +2. 额外删除同名 `.meta` sidecar + +然后: + +- 若当前选中的正是这个路径,会先清空选择 +- 最后调用 [RefreshCurrentFolder](RefreshCurrentFolder.md) + +删除 sidecar 的目的是让资源标识与导入元数据和资源本体一起消失,避免删掉文件后仍残留孤立的 `.meta`。 + +## 返回值语义 + +- 删除并刷新成功时返回 `true` +- 任一前置校验失败或文件系统异常时返回 `false` + +## 注意事项 + +- 当前不会弹出冲突确认或预览 +- 删除目录时依赖 `remove_all(...)` 递归删除内容 +- 只要路径越出项目根目录,当前就会直接拒绝 + +## 相关文档 + +- [ProjectManager](ProjectManager.md) +- [MoveItem](MoveItem.md) +- [RenameItem](RenameItem.md) diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/Initialize.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/Initialize.md new file mode 100644 index 00000000..40964fe4 --- /dev/null +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/Initialize.md @@ -0,0 +1,41 @@ +# ProjectManager::Initialize + +```cpp +void Initialize(const std::string& projectPath); +``` + +## 当前行为 + +`Initialize(...)` 会把传入路径记为 `m_projectPath`,然后把 `/Assets` 作为当前项目浏览根目录。 + +按当前实现: + +1. 计算 `/Assets` +2. 若不存在,则创建: + - `Assets/` + - `Assets/Scenes/` +3. 调用内部 `ScanDirectory(...)` 递归构建根目录树 +4. 把根节点名称强制设为 `"Assets"` +5. 重置 `m_path` 为只包含根目录的一层 +6. 清空当前选择 + +## 失败退化 + +若扫描或建目录过程中抛异常,当前不会把初始化直接判成失败,而是退化为: + +- 构造一个最小可用的 `Folder` 根节点 +- 仍把名字设为 `"Assets"` +- 仍重置路径栈与当前选择 + +也就是说,`ProjectManager` 更偏向“尽量给 ProjectPanel 一个可工作的空根目录”,而不是把异常往上抛。 + +## 注意事项 + +- 当前只保证 `Assets/Scenes`,不会自动生成旧文档里提到的一整套示例子目录。 +- 初始化后当前位置总是根目录,不会保留旧会话中的最后浏览路径。 + +## 相关文档 + +- [ProjectManager](ProjectManager.md) +- [RefreshCurrentFolder](RefreshCurrentFolder.md) +- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/MoveItem.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/MoveItem.md new file mode 100644 index 00000000..e375ab46 --- /dev/null +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/MoveItem.md @@ -0,0 +1,49 @@ +# ProjectManager::MoveItem + +```cpp +bool MoveItem(const std::string& sourceFullPath, const std::string& destFolderFullPath); +``` + +## 当前行为 + +`MoveItem(...)` 当前只支持“把一个项目内条目移动到另一个项目内目录”。 + +它会校验: + +- 源路径和目标目录路径都非空 +- `m_rootFolder` 已存在 +- 源路径存在 +- 目标路径存在且是目录 +- 源和目标都位于项目根目录之下 +- 源路径不是根目录本身 +- 若源是目录,目标不能位于它自己的子树里 +- 目标最终路径不能与源路径相同 +- 目标最终路径不能已存在同名条目 + +## 成功路径 + +校验通过后,当前会: + +1. 把目标路径拼成 `destFolder / source.filename()` +2. 对资源本体执行 `fs::rename(sourcePath, destPath)` +3. 若存在同名 `.meta` sidecar,则同步移动 `.meta` +4. 调用 [RefreshCurrentFolder](RefreshCurrentFolder.md) + +同步移动 sidecar 是当前资源移动行为里很重要的一部分,因为它让导入元数据和资源路径保持一致,不会把 `.meta` 留在旧目录。 + +## 返回值语义 + +- 移动成功并刷新完成时返回 `true` +- 任一校验失败或文件系统异常时返回 `false` + +## 当前限制 + +- 当前不做自动重命名避让;目标已有同名条目会直接失败 +- 当前不会主动保留被移动条目的选择状态;若路径变化导致旧 `m_selectedItemPath` 失效,刷新后选择可能被清掉 + +## 相关文档 + +- [ProjectManager](ProjectManager.md) +- [DeleteItem](DeleteItem.md) +- [RenameItem](RenameItem.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/ProjectManager.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/ProjectManager.md index 24868c2a..e22e29af 100644 --- a/docs/api/XCEngine/Editor/Managers/ProjectManager/ProjectManager.md +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/ProjectManager.md @@ -6,35 +6,203 @@ **源文件**: `editor/src/Managers/ProjectManager.h` -**描述**: `IProjectManager` 的默认实现,负责扫描项目 `Assets` 目录、维护面包屑路径,并支持基础文件夹与文件操作。 +**描述**: `IProjectManager` 的默认实现,负责把项目 `Assets` 目录扫描成 `AssetItem` 树,并提供路径导航、选择同步与基础文件操作。 ## 概述 -`ProjectManager` 当前本质上是一个面向 `Assets` 目录的轻量文件系统浏览器。 +`ProjectManager` 当前不是 `AssetDatabase`,也不是资源导入服务;但它也已经不只是“轻量文件浏览器”。 -它负责: +按当前实现,它承担三类稳定职责: -- 初始化项目目录结构 -- 扫描目录并构建 `AssetItem` 树 -- 维护当前路径栈 -- 创建文件夹、删除项、移动项 +- 把 `/Assets` 扫描为 `AssetItem` 树,供 [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) 消费 +- 维护浏览路径、当前选择与刷新后的状态恢复 +- 处理创建、删除、移动、重命名等基础文件系统操作 -## 当前实现说明 +因此它更准确的定位是: -- `Initialize(projectPath)` 会确保 `/Assets` 以及若干默认子目录存在。 -- 若第一次初始化发现目录不存在,会自动创建 `Textures/Models/Scripts/Materials/Scenes` 等目录,并生成少量示例文件。 -- `GetCurrentItems()` 返回当前路径末尾节点的 `children`。 -- 目录扫描时,文件夹会排在文件前面,并按名字排序。 -- 资源类型当前按扩展名启发式推断,例如 `.png -> Texture`、`.fbx -> Model`、`.cs/.cpp/.h -> Script`。 +- editor 侧项目文件系统投影层 +- Project Browser 的默认执行层 -## 当前实现边界 +而不是 engine 侧资产数据库本身。 -- 目前是实时扫描树,不是文件系统监听模式。 -- 异常处理大多是吞掉异常并保持当前状态,错误反馈很弱。 -- 类型判断完全靠扩展名映射,不依赖资源导入数据库。 +## 内部状态模型 + +当前长期持有的核心状态只有四类: + +| 状态 | 作用 | +|------|------| +| `m_rootFolder` | 当前 `Assets` 根节点。 | +| `m_path` | 当前浏览路径的目录节点栈,也是面包屑来源。 | +| `m_selectedItemPath` | 当前选中项的完整路径字符串。 | +| `m_projectPath` | 当前项目根目录。 | + +这套设计的直接收益是: + +- 刷新树时,不需要保留旧 `AssetItem` 指针身份 +- 只要路径还能重新解析,当前目录和当前选中项就能在重建后恢复 + +## 扫描与类型推断 + +### 根目录初始化 + +[Initialize](Initialize.md) 当前固定把 `/Assets` 当作根目录: + +- 若目录不存在,会先创建 `Assets/` 和 `Assets/Scenes/` +- 根节点名称固定重写为 `Assets` +- 初始化后 `m_path` 只包含根节点 + +它不会额外创建旧文档里提到的演示子目录,也不会在这里预热资产数据库。 + +### 递归扫描 + +`ScanDirectory(...)` 当前会: + +- 遍历目录项 +- 对子目录递归构建子 `AssetItem` +- 跳过显示层面的 `.meta` 文件 +- 把目录排在文件前面,再按名称排序 + +这意味着 `ProjectPanel` 看到的是“过滤掉 `.meta` 侧车文件后的浏览树”,而不是磁盘上的逐项镜像。 + +### 类型推断 + +文件项当前主要按扩展名启发式推断: + +| 扩展名 | 当前类型 | +|------|------| +| 图片扩展名 | `Texture` | +| `.fbx/.obj/.gltf/.glb` | `Model` | +| `.cs/.cpp/.h` | `Script` | +| `.mat` | `Material` | +| `.xc/.unity/.scene` | `Scene` | +| `.prefab` | `Prefab` | +| 其他 | `File` | + +同时还会记录: + +- `extensionLower` +- `isImageAsset` +- `canUseImagePreview` + +供 `ProjectPanel` 的图标与缩略图策略使用。 + +## 路径导航与选择恢复 + +### 路径导航 + +当前路径相关公开 API 基本都围绕 `m_path` 工作: + +- `NavigateToFolder(...)` + - 重新解析从根到目标目录的完整路径 + - 成功后清空当前选择 +- `NavigateBack()` + - 只在 `m_path.size() > 1` 时后退 + - 后退后清空当前选择 +- `NavigateToIndex(index)` + - 直接截断面包屑到指定层级 + - 同样会清空当前选择 + +`GetCurrentPath()` 始终返回从 `"Assets"` 开始的逻辑路径,而不是磁盘绝对路径。 + +### 选择恢复 + +`ProjectManager` 当前把选择保存成 `m_selectedItemPath`,而不是保留旧 `AssetItemPtr`。 + +因此: + +- `RefreshCurrentFolder()` 重建目录树后,只要同一路径仍存在,选择可以恢复 +- 若原路径不存在,则 `SyncSelection()` 自动清除选择 + +这也是 `ProjectSelectionSurvivesRefreshWhenItemOrderChanges` 能成立的根本原因。 + +## 基础文件操作 + +当前真正值得文档化的,不是 getter / setter,而是这几组带规则的写操作。 + +### 创建目录 + +[CreateFolder](CreateFolder.md) 当前会: + +- trim 名称 +- 拒绝空名、`.`、`..` 和 Windows 非法文件名字符 +- 在当前目录下生成唯一目录名,如 `New Folder 1` +- 刷新树并自动选中新目录 + +### 删除资源 + +[DeleteItem](DeleteItem.md) 当前会: + +- 拒绝空路径、根目录本身和项目根之外的路径 +- 删除目标本体 +- 额外删除同名 `.meta` sidecar +- 必要时清空选择 +- 刷新目录树 + +这里删除 `.meta` 并不是“顺手清理垃圾文件”,而是为了避免资源本体被移除后仍残留旧 GUID / 导入元数据。 + +### 移动资源 + +[MoveItem](MoveItem.md) 当前会: + +- 要求源和目标都位于当前 `Assets` 根目录之下 +- 要求目标是已存在目录 +- 禁止移动根目录本身 +- 禁止把目录移动到自己的子目录 +- 禁止覆盖已有同名目标 +- 成功后同步移动 `.meta` sidecar + +同步移动 sidecar 的意义同样是保持资源身份与导入配置跟随资源本体一起迁移,而不是把 `.meta` 留在旧路径。 + +### 重命名资源 + +[RenameItem](RenameItem.md) 当前会: + +- trim 并校验新名字 +- 对文件项在“不写扩展名”时自动保留原扩展名 +- 支持 Windows 下 case-only rename +- 成功后同步重命名 `.meta` sidecar +- 若被重命名项正好是当前选中项,会同步更新 `m_selectedItemPath` + +因此当前重命名语义已经不只是 UI 上改显示名,而是显式维护资源路径、sidecar 和选择状态的一致性。 + +## 公开方法 + +| 方法 | 说明 | +|------|------| +| [Initialize](Initialize.md) | 初始化项目根、确保 `Assets/Scenes` 存在并建立根目录树。 | +| [RefreshCurrentFolder](RefreshCurrentFolder.md) | 重建目录树并尽量保留当前路径与当前选择。 | +| [CreateFolder](CreateFolder.md) | 在当前目录创建唯一命名的新文件夹。 | +| [DeleteItem](DeleteItem.md) | 删除项目内资源或目录,并移除同名 `.meta` sidecar。 | +| [MoveItem](MoveItem.md) | 在项目目录内移动资源或目录,并同步移动 `.meta` sidecar。 | +| [RenameItem](RenameItem.md) | 重命名资源或目录,支持保留扩展名与 case-only rename。 | + +其余 getter / setter / 导航方法当前语义较直接,类型页中已概述,不再逐个拆页。 + +## 测试与行为锚点 + +和当前 `ProjectManager` 直接相关的测试锚点主要在 `tests/editor/test_action_routing.cpp`: + +- `ProjectCommandsCreateFolderMoveAssetAndOpenFolderHelper` +- `ProjectCommandsCreateFolderUsesUniqueDefaultName` +- `ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension` +- `ProjectCommandsRejectInvalidMoveTargets` +- `ProjectSelectionSurvivesRefreshWhenItemOrderChanges` +- `ProjectCommandsRejectMovingFolderIntoItsDescendant` + +这些测试虽然多数通过 `ProjectCommands` 外层 helper 进入,但已经把 `ProjectManager` 的真实文件系统行为钉得比较牢。 + +## 当前限制 + +- 当前仍是主动扫描树,不是文件系统监听器模型。 +- 类型识别仍然是扩展名启发式,不直接查询资产数据库。 +- 目录扫描和大部分文件系统错误当前都以“吞异常并保持可用”为主,诊断能力较弱。 +- `.meta` 文件只在变更操作时被显式跟随处理,不会单独显示在浏览树里。 ## 相关文档 - [Managers](../Managers.md) - [IProjectManager](../../Core/IProjectManager/IProjectManager.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) +- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) - [AssetItem](../../Core/AssetItem/AssetItem.md) +- [AssetDatabase](../../../Core/Asset/AssetDatabase/AssetDatabase.md) diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/RefreshCurrentFolder.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/RefreshCurrentFolder.md new file mode 100644 index 00000000..7879f29c --- /dev/null +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/RefreshCurrentFolder.md @@ -0,0 +1,38 @@ +# ProjectManager::RefreshCurrentFolder + +```cpp +void RefreshCurrentFolder(); +``` + +## 当前行为 + +这个方法当前不是“只刷新当前目录 children 列表”,而是调用内部 `RebuildTreePreservingPath()` 重建整棵 `Assets` 树,然后尽量恢复当前浏览状态。 + +内部主要流程是: + +1. 记录旧的目录路径栈 `m_path` +2. 重新从 `/Assets` 扫描整棵树 +3. 重新解析旧路径栈,尽量恢复当前面包屑位置 +4. 通过 `SyncSelection()` 重新校验当前 `m_selectedItemPath` + +## 保留语义 + +- 若原浏览路径中的每一级目录仍然存在,则当前目录会被保留 +- 若中间某一级目录已不存在,则恢复过程会在该层停止 +- 若当前选中项路径仍能在当前目录项中重新解析,选择会保留 +- 若当前选中项已不存在,则选择会被清空 + +这也是当前刷新后即使目录项顺序变化,选择仍可恢复的原因。 + +## 失败语义 + +当前实现把异常吞掉,因此: + +- 刷新失败时不会抛异常 +- 也不会提供错误返回值 +- 上层看到的通常是“保持旧状态” + +## 相关文档 + +- [ProjectManager](ProjectManager.md) +- [Initialize](Initialize.md) diff --git a/docs/api/XCEngine/Editor/Managers/ProjectManager/RenameItem.md b/docs/api/XCEngine/Editor/Managers/ProjectManager/RenameItem.md new file mode 100644 index 00000000..f775826e --- /dev/null +++ b/docs/api/XCEngine/Editor/Managers/ProjectManager/RenameItem.md @@ -0,0 +1,66 @@ +# ProjectManager::RenameItem + +```cpp +bool RenameItem(const std::string& sourceFullPath, const std::string& newName); +``` + +## 当前行为 + +`RenameItem(...)` 当前支持目录与文件重命名,但文件和目录的规则并不完全一样。 + +前置校验包括: + +- `sourceFullPath` 非空 +- `m_rootFolder` 已存在 +- 新名字在 trim 后仍非空 +- 新名字不是 `.` / `..` +- 新名字不包含 Windows 非法字符 +- 源路径存在且位于项目根目录之下 +- 源路径不是根目录本身 + +## 文件与目录的差异 + +### 目录 + +- 目录直接使用 trim 后的新名字 + +### 文件 + +当前会先走内部 `BuildRenamedEntryName(...)`: + +- 若新名字本身已经带扩展名,就直接使用该文件名 +- 若新名字没带扩展名,则自动保留原文件扩展名 + +这也是 `ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension` 这个测试成立的原因。 + +## case-only rename + +Windows 下大小写不敏感,直接把 `foo.txt` 改成 `Foo.txt` 往往不能按普通重命名处理。 + +当前实现专门走了 `RenamePathCaseAware(...)`: + +- 普通路径变化时直接 `fs::rename(...)` +- 仅大小写变化时,先改到一个临时路径,再改到目标路径 + +因此 case-only rename 当前是被显式支持的。 + +## 成功路径 + +真正重命名成功后,当前还会: + +1. 同步重命名 `.meta` sidecar +2. 若当前选中的正是原路径,则把 `m_selectedItemPath` 更新到新路径 +3. 调用 [RefreshCurrentFolder](RefreshCurrentFolder.md) + +这意味着当前实现维护的是“资源文件 + sidecar + 当前选择”三者一致性,而不只是单纯改一个文件名。 + +## 返回值语义 + +- 重命名成功并刷新完成时返回 `true` +- 任一校验失败、目标名非法或文件系统异常时返回 `false` + +## 相关文档 + +- [ProjectManager](ProjectManager.md) +- [MoveItem](MoveItem.md) +- [DeleteItem](DeleteItem.md) diff --git a/docs/api/XCEngine/Editor/panels/ProjectPanel/Constructor.md b/docs/api/XCEngine/Editor/panels/ProjectPanel/Constructor.md new file mode 100644 index 00000000..d955fe7a --- /dev/null +++ b/docs/api/XCEngine/Editor/panels/ProjectPanel/Constructor.md @@ -0,0 +1,45 @@ +# ProjectPanel::ProjectPanel + +**命名空间**: `XCEngine::Editor` + +**类型**: `constructor` + +**源文件**: `editor/src/panels/ProjectPanel.h` + +## 签名 + +```cpp +ProjectPanel(); +``` + +## 作用 + +创建标题固定为 `"Project"` 的资源浏览面板。 + +## 当前实现行为 + +当前构造函数非常薄: + +```cpp +ProjectPanel::ProjectPanel() : Panel("Project") { +} +``` + +也就是说它只完成: + +- 通过基类 `Panel` 设置面板标题 + +其余状态都由成员默认值负责初始化,例如: + +- `m_searchBuffer` +- `m_navigationWidth` +- `m_folderTreeState` +- `m_renameState` +- `m_assetDragDropState` +- `m_deferredContextAction` + +## 相关文档 + +- [ProjectPanel](ProjectPanel.md) +- [Initialize](Initialize.md) +- [Render](Render.md) diff --git a/docs/api/XCEngine/Editor/panels/ProjectPanel/Initialize.md b/docs/api/XCEngine/Editor/panels/ProjectPanel/Initialize.md new file mode 100644 index 00000000..58f67b10 --- /dev/null +++ b/docs/api/XCEngine/Editor/panels/ProjectPanel/Initialize.md @@ -0,0 +1,41 @@ +# ProjectPanel::Initialize + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/panels/ProjectPanel.h` + +## 签名 + +```cpp +void Initialize(const std::string& projectPath); +``` + +## 作用 + +把指定项目路径的初始化工作交给当前 `IProjectManager`。 + +## 当前实现行为 + +当前实现只有一行: + +```cpp +m_context->GetProjectManager().Initialize(projectPath); +``` + +这说明 `ProjectPanel` 不自己扫描目录、构建资产树或维护项目数据库;它只负责把“项目浏览器应切到哪个项目”这件事转交给 manager。 + +## 当前假设 + +- `m_context` 已经有效 +- `m_context->GetProjectManager()` 已经可用 + +因此它更像“面板外壳的初始化桥接”,而不是完整的项目加载入口。 + +## 相关文档 + +- [ProjectPanel](ProjectPanel.md) +- [Constructor](Constructor.md) +- [Render](Render.md) +- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) diff --git a/docs/api/XCEngine/Editor/panels/ProjectPanel/ProjectPanel.md b/docs/api/XCEngine/Editor/panels/ProjectPanel/ProjectPanel.md index e4846b87..54f5d95a 100644 --- a/docs/api/XCEngine/Editor/panels/ProjectPanel/ProjectPanel.md +++ b/docs/api/XCEngine/Editor/panels/ProjectPanel/ProjectPanel.md @@ -6,121 +6,263 @@ **源文件**: `editor/src/panels/ProjectPanel.h` -**描述**: 编辑器项目资源浏览面板,负责目录树、面包屑、搜索框、资源卡片和拖放交互的前端呈现。 +**描述**: 项目资源浏览面板,负责把 `IProjectManager` 当前目录模型渲染成“目录树 + breadcrumb + 搜索 + 资源网格 + 上下文菜单 + 拖放”的 Project Browser。 ## 概述 -`ProjectPanel` 是当前 Editor 中最接近“资源浏览器”的面板。它本身不维护项目资源数据库,而是把 `IProjectManager` 暴露的数据模型转成一个典型的双栏浏览界面: +`ProjectPanel` 当前是 Editor 里项目资源浏览的前端外壳,但它不是资产数据库本身。 -- 左侧是目录树导航。 -- 右侧是当前目录的资源浏览区。 -- 顶部是搜索框与面包屑。 +它的职责边界比较明确: -如果和 Unity 对照,可以把它理解成一个更轻量、当前聚焦于基础浏览和拖放的 Project Browser。 +- 项目目录模型与选择状态来自 [IProjectManager](../../Core/IProjectManager/IProjectManager.md) +- 资源打开、创建、删除、重命名、移动等语义落到 [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) +- 少量通用交互协议由 [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md) 提供 +- 面板自身主要负责布局、即时模式 UI 状态和延迟动作收口 -## 当前实现行为 +## 生命周期与公开入口 -按 `editor/src/panels/ProjectPanel.cpp` 的实现: +- [Constructor](Constructor.md) + 创建标题固定为 `"Project"` 的面板。 +- [Initialize](Initialize.md) + 把项目路径初始化工作委托给当前 `IProjectManager`。 +- [Render](Render.md) + 执行完整的项目浏览器绘制与交互驱动。 -- `Initialize(projectPath)` 直接委托给 `m_context->GetProjectManager().Initialize(projectPath)`。 -- `Render()` 会: - - 建立标准 `PanelWindowScope`。 - - 把焦点动作路由标记为 `EditorActionRoute::Project`。 - - 先渲染 toolbar。 - - 再创建左右分栏内容区。 - - 使用 `UI::DrawSplitter(...)` 支持调整左侧导航栏宽度。 - - 收尾时绘制“新建文件夹”对话框。 +## 面板内部状态 -当前导航栏宽度会被显式夹紧在一个合理范围内: +`ProjectPanel` 当前长期持有的主要是 UI 状态,而不是项目数据本体: -- 最小值来自 `ProjectNavigationMinWidth()` -- 右侧浏览区最小宽度来自 `ProjectBrowserMinWidth()` +| 状态 | 作用 | +|------|------| +| `m_searchBuffer` | 当前目录搜索关键字 | +| `m_navigationWidth` | 左侧目录树宽度 | +| `m_folderTreeState` | 目录树展开状态 | +| `m_renameState` | 行内重命名状态 | +| `m_assetDragDropState` | 本帧拖放源 / 目标状态 | +| `m_deferredContextAction` | 延迟到本帧末尾执行的上下文菜单动作 | -这说明当前实现已经从一开始就按“可调整工作区”的编辑器思路组织,而不是固定死尺寸。 +## 初始化与每帧主流程 + +### [Initialize(projectPath)](Initialize.md) + +当前实现只有一行: + +```cpp +m_context->GetProjectManager().Initialize(projectPath); +``` + +这说明 `ProjectPanel` 不自己扫描目录,也不自己构建项目树。 + +### [Render()](Render.md) + +当前每帧主要顺序是: + +1. 打开 `PanelWindowScope` +2. 调用 `ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project)` +3. `BeginAssetDragDropFrame()` +4. 渲染顶部 toolbar +5. 渲染左侧目录树与右侧浏览区 +6. `FinalizeAssetDragDrop(manager)` +7. 在帧末执行 `m_deferredContextAction` + +这里最关键的两个点是: + +- Project 面板会显式声明当前焦点 route +- 拖放提交和上下文菜单命令都在本帧尾部统一收口,避免打断当前 UI 遍历 + +## 顶部工具栏 + +`RenderToolbar()` 当前负责两件事: + +- 左侧显示当前 `ResourceManager` 的 project asset import 状态 +- 右侧提供当前目录搜索框 + +导入状态文本和 tooltip 来自: + +- `ResourceManager::GetProjectAssetImportStatus()` +- `AssetImportService::ImportStatusSnapshot` + +搜索行为则比较克制: + +- 只过滤 `manager.GetCurrentItems()` 返回的当前目录条目 +- 匹配逻辑使用 `UI::SearchQuery::Matches(item->name)` +- 不做全项目搜索、类型过滤或标签过滤 ## 左侧目录树 -左侧目录树是当前 `ProjectPanel` 最典型的共享 UI 基建使用者之一: +### 数据来源 -- 根节点来自 `IProjectManager::GetRootFolder()`。 -- 当前目录来自 `IProjectManager::GetCurrentFolder()`。 -- 每个目录节点都通过 `UI::DrawTreeNode(...)` 绘制。 -- 节点样式使用 `UI::ProjectFolderTreeStyle()`。 -- 节点前缀使用 `UI::DrawAssetIcon(..., AssetIconKind::Folder)`。 -- `defaultOpen` 会根据 `IsCurrentTreeBranch(...)` 判断当前目录是否位于该目录分支下。 -- 展开状态保存在 `m_folderTreeState` 中。 +目录树主要消费: -用户单击树节点时,会立即调用 `manager.NavigateToFolder(folder)` 完成导航;右键则通过 `Actions::HandleProjectItemContextRequest(...)` 打开对应上下文菜单。 +- `GetRootFolder()` +- `GetCurrentFolder()` + +### 节点绘制与交互 + +每个目录节点当前通过 `UI::DrawTreeNode(...)` 绘制,并带有: + +- 当前目录高亮 +- 基于 `IsCurrentTreeBranch(...)` 的默认展开 +- 自定义 folder icon prefix + +当前支持的主要交互包括: + +- 左键点击目录 -> `NavigateToFolder(folder)` +- 目录作为资源拖放目标 +- 空白区右键弹出项目上下文菜单 + +若根目录不可用,则显示 `No Assets Folder` 空状态。 ## 右侧浏览区 -右侧浏览区分成两层: +右侧浏览区分成两部分: -- Header:显示面包屑导航,并在底部画一条 divider。 -- Body:以资源网格形式显示当前目录下通过搜索过滤后的条目。 +- `RenderBrowserHeader(manager)` +- `RenderBrowserPane(manager)` 的资源网格主体 -资源卡片当前通过 `UI::DrawAssetTile(...)` 绘制,并根据 `item->isFolder` 区分: +### Breadcrumb -- `Folder` 图标 -- `File` 图标 +Header 当前通过 `UI::DrawToolbarBreadcrumbs(...)` 渲染路径,并消费: -交互结果统一收集到 `AssetItemInteraction` 中,再在循环结束后统一处理。这种写法有一个很实用的好处:可以避免在绘制过程中直接修改当前迭代容器或导航状态,让即时模式 UI 代码更稳定。 +- `manager.GetPathDepth()` +- `manager.GetPathName(index)` +- `manager.NavigateToIndex(index)` -## 搜索与导航 +因此目录导航当前主要依赖目录树和 breadcrumb,而不是单独的“后退按钮”。 -当前搜索实现比较明确,也需要在文档里写清楚: +### 资源网格 -- 搜索框位于 toolbar 右侧。 -- 搜索只对 `manager.GetCurrentItems()` 返回的当前目录条目生效。 -- 匹配逻辑是大小写不敏感的子串匹配。 -- 当前没有全文索引、标签过滤或类型过滤。 +主体区域当前会: -面包屑则通过 `UI::DrawToolbarBreadcrumbs(...)` 实现,点击非当前段会调用 `manager.NavigateToIndex(index)`。 +1. 先基于搜索关键字计算 `visibleItems` +2. 动态估算卡片高度与列数 +3. 逐项调用 `RenderAssetItem(...)` +4. 在帧末统一处理选中、打开与空白区清选 -## 拖放与上下文菜单 +`RenderAssetItem(...)` 当前支持: -当前 `ProjectPanel` 并不自己实现拖放协议和命令执行,而是按职责分层调用其他模块: +- 单击选中 +- 双击打开 +- 右键选中并弹出上下文菜单 +- 纹理预览与图标 fallback +- 行内重命名 +- 拖放源 / 目录拖放目标 -- `Actions::BeginProjectAssetDrag(item, iconKind)` 负责发起拖拽源。 -- `Actions::AcceptProjectAssetDropPayload(item)` 负责检测是否有资源被拖到当前目标上。 -- 若确实发生移动,则调用 `Commands::MoveAssetToFolder(...)`。 -- 右键菜单与空白区域菜单由 `Actions::*ContextPopup(...)` 系列 helper 负责绘制。 +若搜索结果为空,会显示: -这种组织方式和 `HierarchyPanel` 一样,遵循的是“面板负责展示与采样,动作层 / 命令层负责实际编辑行为”的编辑器架构。 +- `No Search Results` +- `No assets match the current search` -## 生命周期与线程语义 +## 上下文菜单与重命名 -- 面板本身持有的是 UI 状态:搜索文本、导航栏宽度、目录树展开状态、弹窗状态。 -- 真实项目数据由 `IProjectManager` 持有。 -- 整套逻辑应视为编辑器 UI 主线程代码。 +### 行内重命名 -## 设计说明 +当前重命名由 `m_renameState` 驱动: -当前 `ProjectPanel` 的设计方向是合理的,因为它先把“资源浏览器”拆成几个稳定的基础体验单元: +1. `BeginRename(item)` 激活编辑状态 +2. `DrawInlineRenameFieldAt(...)` 覆盖卡片 label 区 +3. 提交时调用 `CommitRename(...)` +4. 真正的重命名落到 `Commands::RenameAsset(...)` -- 分栏布局 -- 目录树 -- 面包屑 -- 资源网格 -- 拖放与上下文菜单 +若用户没有实际修改显示名称,`CommitRename(...)` 会直接结束重命名,而不会重复提交文件操作。 -这样做的好处是,后续无论是加缩略图、资源导入器状态、筛选器还是收藏夹,都可以在现有骨架上演进,而不是推倒重来。 +### 右键菜单 + +`DrawProjectContextMenu(...)` 当前支持: + +- `Create -> Folder` +- `Create -> Material` +- `Show in Explore` +- `Open` +- `Delete` +- `Rename` +- `Copy Path` + +其中创建资源采用 deferred action: + +1. 必要时先导航到目标目录 +2. 调用 `Commands::CreateFolder(...)` 或 `Commands::CreateMaterial(...)` +3. 新建成功后自动进入重命名状态 + +## 拖放与移动资源 + +当前拖放逻辑被收口成一套帧内状态机,而不是散落在每个 tile 里。 + +### 帧开始 + +`BeginAssetDragDropFrame()` 会: + +- 清空 `m_assetDragDropState` +- 通过 `Actions::GetDraggedProjectAssetPath()` 识别当前拖拽源 + +### 目录作为 drop target + +`RegisterFolderDropTarget(...)` 会: + +- 只接受目录目标 +- 校验 payload 类型为 `ProjectAssetPayloadType()` +- 用 `Commands::CanMoveAssetToFolder(...)` 判断是否合法 +- 在真正投递时记录源路径与目标目录 + +### 帧结束统一提交 + +`FinalizeAssetDragDrop(...)` 会: + +- 根据当前悬停是否合法切换鼠标样式 +- 若本帧成功投递,则统一调用 `Commands::MoveAssetToFolder(...)` + +这样可以避免在 UI 遍历过程中直接修改底层目录树。 + +## 与项目工作流命令的关系 + +`ProjectPanel` 当前直接消费的主要是资源级命令: + +- `CreateFolder` +- `CreateMaterial` +- `DeleteAsset` +- `RenameAsset` +- `MoveAssetToFolder` +- `OpenAsset` + +它**不**负责发起以下项目级入口: + +- `Save Project` +- `Open Project...` +- `New Project...` +- `Rebuild Script Assemblies` + +这些工作流分别由主菜单和命令层收口。 + +当外部命令触发 `RefreshCurrentFolder()` 时,面板会在下一帧反映新的目录树状态。 + +## 测试锚点 + +`tests/editor/test_action_routing.cpp` 当前与这条资源链路直接相关的测试包括: + +- `ProjectCommandsCreateFolderMoveAssetAndOpenFolderHelper` +- `ProjectCommandsCreateFolderUsesUniqueDefaultName` +- `ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension` +- `ProjectCommandsRejectInvalidMoveTargets` +- `ProjectSelectionSurvivesRefreshWhenItemOrderChanges` +- `ProjectCommandsRejectMovingFolderIntoItsDescendant` ## 当前限制 -- 搜索只在当前目录里做前端子串过滤,不是全项目索引搜索。 -- 当前没有资源缩略图生成或异步预览系统,图标仍以 `BuiltInIcons` 和简单卡片为主。 -- 文件树展开状态只保存在内存中,不会自动跨重启恢复。 -- 拖放移动是直接命令式处理,没有更复杂的批处理或事务预览。 +- 搜索当前只覆盖当前目录的名称匹配。 +- 目录树展开状态当前只保存在内存里。 +- 拖拽预览 tooltip 被关闭,拖放反馈比较轻量。 +- 资源上下文菜单仍偏基础,没有批量操作或复杂预览。 +- 面板本身不是项目保存或脚本重建入口。 ## 相关文档 +- [Constructor](Constructor.md) +- [Initialize](Initialize.md) +- [Render](Render.md) - [panels](../panels.md) +- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) - [IProjectManager](../../Core/IProjectManager/IProjectManager.md) - [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) - [AssetItem](../../Core/AssetItem/AssetItem.md) -- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md) -- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) -- [TreeView](../../UI/TreeView/TreeView.md) -- [BuiltInIcons](../../UI/BuiltInIcons/BuiltInIcons.md) -- [SplitterChrome](../../UI/SplitterChrome/SplitterChrome.md) +- [Widgets](../../UI/Widgets/Widgets.md) diff --git a/docs/api/XCEngine/Editor/panels/ProjectPanel/Render.md b/docs/api/XCEngine/Editor/panels/ProjectPanel/Render.md new file mode 100644 index 00000000..76df662a --- /dev/null +++ b/docs/api/XCEngine/Editor/panels/ProjectPanel/Render.md @@ -0,0 +1,94 @@ +# ProjectPanel::Render + +**命名空间**: `XCEngine::Editor` + +**类型**: `method` + +**源文件**: `editor/src/panels/ProjectPanel.h` + +## 签名 + +```cpp +void Render() override; +``` + +## 作用 + +绘制完整的 Project Browser,并驱动目录树、搜索、资源网格、拖放移动和延迟上下文菜单动作。 + +## 当前实现行为 + +### 1. 建立面板与焦点路由 + +当前会先: + +- 打开 `UI::PanelWindowScope` +- 调用 `Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project)` + +这保证 Project 面板获得焦点时,项目相关动作和 Inspector 资源检查模式都能看到正确 route。 + +### 2. 初始化本帧拖放状态 + +每帧开始时会: + +- `BeginAssetDragDropFrame()` +- 清空 `m_deferredContextAction` +- 渲染顶部 toolbar + +当前这个 toolbar 不只是搜索框,还会读取 `ResourceManager::GetProjectAssetImportStatus()`,在左侧显示 `Library: ...` 状态文案,并在 hover 时展开导入状态 tooltip。 + +拖放不是在单个 tile 里直接提交,而是作为整帧状态机统一收口。 + +### 3. 组织双栏主体布局 + +主体内容当前包括: + +- 左侧目录树 pane +- 中间 splitter +- 右侧浏览区 pane + +其中 `m_navigationWidth` 会在当前帧按: + +- 最小导航宽度 +- 最小浏览区宽度 + +进行夹取,避免 splitter 把任一侧拖到不可用。 + +### 4. 渲染目录树与浏览区 + +左侧目录树通过 `RenderFolderTreePane(manager)` 完成,右侧浏览区通过 `RenderBrowserPane(manager)` 完成。 + +浏览区内部进一步包括: + +- 面包屑 header +- 搜索过滤后的资源卡片网格 +- 行内重命名 +- 右键上下文菜单 + +### 5. 统一提交拖放与 deferred action + +当前渲染末尾会依次执行: + +1. `FinalizeAssetDragDrop(manager)` +2. 若存在 `m_deferredContextAction`,则在本帧最后执行 + +这意味着: + +- 资源移动不会在 UI 遍历过程中直接打断当前逻辑 +- 创建资源、重命名、Show in Explorer 这类上下文菜单动作也会延后到本帧收尾阶段 + +## 设计含义 + +`Render()` 当前承担的是“项目浏览器壳层主流程”,而不是具体资源命令实现。真正的打开、创建、删除、重命名和移动语义仍下沉在: + +- `Actions::*` +- `Commands::*` +- `IProjectManager` + +## 相关文档 + +- [ProjectPanel](ProjectPanel.md) +- [Constructor](Constructor.md) +- [Initialize](Initialize.md) +- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) +- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md)