docs: sync project browser docs

This commit is contained in:
2026-04-04 01:02:57 +08:00
parent 42e2e1b8f2
commit 2abe0bbbd4
30 changed files with 1725 additions and 116 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -6,37 +6,91 @@
**源文件**: `editor/src/Core/EditorConsoleSink.h` **源文件**: `editor/src/Core/EditorConsoleSink.h`
**描述**: 编辑器专用日志 sink引擎日志桥接到 Console 面板可读取的内存缓冲区 **描述**: 编辑器专用日志 sink `Logger` 的输出桥接成带 `serial` / `revision` 的内存快照,供 `ConsolePanel` 增量消费
## 概述 ## 概述
`EditorConsoleSink` 虽然位于 `editor/src/Core`,但命名空间属于 `XCEngine::Debug` `EditorConsoleSink` 虽然位于 `editor/src/Core`,但命名空间属于 `XCEngine::Debug`
的定位很明确:作为 `Logger` 的一个输出目标,把日志保存在内存中,供 Console 面板显示。 不是单纯“把日志塞进数组”的轻量容器,而是当前 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”。 内部缓冲区 `m_logs` 保存的是 `EditorConsoleRecord`,而不是裸 `LogEntry`
引擎日志系统不应该知道编辑器面板怎么画 UI但编辑器又确实需要消费同一份日志流。把桥接能力做成一个 `ILogSink` 子类,是最干净的集成方式。
## 当前限制 - `serial` 是 sink 内单调递增的记录编号。
- `entry` 是完整日志条目副本。
- 当前只保留最近 1000 条日志 `ConsolePanel` 当前依赖这个 `serial` 做:
- `GetLogs()` 会复制整个缓冲区
- `SetCallback()` 没有更复杂的线程调度机制,回调触发策略非常轻量 - 新增错误扫描边界
- 列表选中保持
- 折叠/搜索后重新定位选中项
### 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) - [Core](../Core.md)
- [ConsoleActionRouter](../../Actions/ConsoleActionRouter/ConsoleActionRouter.md)
- [ConsolePanel](../../panels/ConsolePanel/ConsolePanel.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)

View File

@@ -0,0 +1,30 @@
# EditorConsoleSink::Flush
满足 `ILogSink` 接口的刷新函数。
```cpp
void Flush() override;
```
## 当前行为
当前实现为空函数,不执行任何操作。
## 原因
`EditorConsoleSink` 只维护内存缓冲区,没有:
- 文件句柄
- socket
- 延迟写盘缓存
所以它不需要像文件 sink 那样做显式落地。
## 返回值
- 无。
## 相关文档
- [返回类型总览](EditorConsoleSink.md)
- [ILogSink](../../../Debug/ILogSink/ILogSink.md)

View File

@@ -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)

View File

@@ -0,0 +1,44 @@
# EditorConsoleSink::GetLogs
返回不含 `serial` 的日志条目快照。
```cpp
std::vector<LogEntry> GetLogs() const;
```
## 当前行为
当前实现会:
1. 在锁内读取 `m_logs`
2. 新建一个 `std::vector<LogEntry>`
3. 把每条 `EditorConsoleRecord::entry` 复制进去
4. 返回这份副本
## 与 GetRecords() 的区别
- [GetLogs](GetLogs.md) 只返回 `LogEntry`
- [GetRecords](GetRecords.md) 保留 `serial`
如果调用方需要做:
- 增量刷新
- 选中保持
- 错误扫描边界
应优先使用 `GetRecords()`
## 成本与限制
- 返回值是整份副本,不是视图。
- 当前调用一次就会复制当前全部缓冲区。
## 返回值
- 当前日志缓冲区的 `LogEntry` 副本数组。
## 相关文档
- [返回类型总览](EditorConsoleSink.md)
- [GetRecords](GetRecords.md)
- [Log](Log.md)

View File

@@ -0,0 +1,41 @@
# EditorConsoleSink::GetRecords
返回带 `serial` 的控制台记录快照。
```cpp
std::vector<EditorConsoleRecord> GetRecords() const;
```
## 当前行为
当前实现非常直接:
1. 在锁内复制 `m_logs`
2. 返回整份 `std::vector<EditorConsoleRecord>` 副本
这也是 [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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,46 @@
# EditorConsoleSink::SetCallback
注册或替换一条“控制台内容变化”回调。
```cpp
void SetCallback(std::function<void()> callback);
```
## 参数
- `callback` - 新的通知回调;可以为空。
## 当前行为
当前实现只是在锁内执行:
- `m_callback = std::move(callback)`
它不会:
- 立即触发新回调
- 保留旧回调形成链式通知
- 做线程切换或延迟调度
## 触发时机
新回调会在以下操作后被调用:
- [Log](Log.md) 成功追加日志
- [Clear](Clear.md) 真正清空非空缓冲区
并且都是在对应调用线程上、解锁之后同步执行。
## 使用建议
如果回调里要刷新 UI 或触发更上层逻辑,调用方应自己保证线程上下文安全,不要假设这里已经切到主线程。
## 返回值
- 无。
## 相关文档
- [返回类型总览](EditorConsoleSink.md)
- [Log](Log.md)
- [Clear](Clear.md)

View File

@@ -0,0 +1,58 @@
# IProjectManager / Current Items And Selection
**命名空间**: `XCEngine::Editor`
**类型**: `interface-group`
**源文件**: `editor/src/Core/IProjectManager.h`
## 相关签名
```cpp
virtual const std::vector<AssetItemPtr>& 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)

View File

@@ -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)

View File

@@ -6,36 +6,82 @@
**源文件**: `editor/src/Core/IProjectManager.h` **源文件**: `editor/src/Core/IProjectManager.h`
**描述**: 定义项目浏览器接口,负责当前资产列表、路径导航、文件夹刷新以及基础资源文件操作。 **描述**: 定义 editor 项目工作流的抽象接口,负责当前资产列表、路径导航、选择状态,以及项目 `Assets` 树上的基础文件操作契约
## 概述 ## 概述
`IProjectManager` 面向的是编辑器 Project 面板这一类工作流 `IProjectManager` 面向的不只是 `ProjectPanel` 的目录浏览,它还是 editor 侧项目资源模型的抽象接口
它的接口组合非常清晰 按当前头文件,它的接口可以分成五组
- 当前目录的资产项列表 - 当前目录的资产项列表
- 当前选中项索引 - 当前选中项与选中路径
- 路径导航 - 路径导航
- 项目根路径 - 项目根路径与目录刷新
- 文件夹刷新/创建/删除/移动 - 创建 / 删除 / 移动 / 重命名
因此它是当前这条链路里的中间抽象层:
```text
MainMenuActionRouter / ProjectPanel
-> ProjectCommands
-> IProjectManager
-> ProjectManager
```
菜单层和面板层都不直接操作文件系统细节,而是通过这个接口进入项目模型与文件操作。
## 核心接口 ## 核心接口
| 方法 | 作用 | | 方法 | 作用 |
|------|------| |------|------|
| `GetCurrentItems()` | 获取当前目录条目。 | | `GetCurrentItems()` | 获取当前目录条目。 |
| `GetSelectedItem()` / `GetSelectedItemPath()` | 获取当前选中资源及其路径。 |
| `GetSelectedIndex()` / `SetSelectedIndex()` | 管理当前选中项索引。 | | `GetSelectedIndex()` / `SetSelectedIndex()` | 管理当前选中项索引。 |
| `SetSelectedItem()` / `ClearSelection()` | 直接管理当前选择。 |
| `NavigateToFolder()` / `NavigateBack()` / `NavigateToIndex()` | 处理路径导航。 | | `NavigateToFolder()` / `NavigateBack()` / `NavigateToIndex()` | 处理路径导航。 |
| `CanNavigateBack()` | 判断能否返回上一级。 | | `CanNavigateBack()` | 判断能否返回上一级。 |
| `GetCurrentPath()` / `GetPathDepth()` / `GetPathName()` | 查询当前路径面包屑。 | | `GetCurrentPath()` / `GetPathDepth()` / `GetPathName()` | 查询当前路径面包屑。 |
| `Initialize()` | 初始化项目目录。 | | `Initialize()` | 初始化项目目录。 |
| `RefreshCurrentFolder()` | 刷新当前目录内容。 | | `RefreshCurrentFolder()` | 刷新当前目录内容。 |
| `CreateFolder()` / `DeleteItem()` / `MoveItem()` | 执行基础文件操作。 | | `CreateFolder()` / `DeleteItem()` / `MoveItem()` / `RenameItem()` | 执行基础文件操作。 |
| `GetProjectPath()` | 获取项目根目录。 | | `GetProjectPath()` | 获取项目根目录。 |
## 边界与职责
`IProjectManager` 当前明确不负责:
- 文件对话框
- 菜单文本或快捷键
- 资产数据库导入 / artifact 构建
- 项目级脚本构建
- 项目保存与项目切换策略
它负责的是同步项目模型与维护动作契约,也就是:
- UI 层能看到什么
- 基础文件操作怎么做
- 当前项目根目录和 `Assets` 浏览树如何对外暴露
这也是当前接口设计上的一个取舍:
- 资源浏览和基础文件操作都通过同步返回值表达结果
- 更高层的项目保存、脚本构建和项目切换仍留给 [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md) 与 `Application`
- 界面层如何呈现这些结果,仍由上层自己决定
## 设计说明
`ProjectPanel` 依赖的目录模型、导航状态和文件操作抽象成接口,有两个直接收益:
- UI 层可以围绕 `AssetItem` 模型稳定开发,而不必直接接触文件系统细节
- 更高层的项目命令层可以只依赖抽象,而不耦合到具体实现
这也是为什么 `IProjectManager` 的当前职责比较克制:它更像“项目资源浏览模型接口”,而不是完整的项目系统门面。
## 相关文档 ## 相关文档
- [Core](../Core.md) - [Core](../Core.md)
- [AssetItem](../AssetItem/AssetItem.md) - [AssetItem](../AssetItem/AssetItem.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md)
- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) - [ProjectManager](../../Managers/ProjectManager/ProjectManager.md)

View File

@@ -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(...)` 会把 `<project>/Assets` 作为根目录,并确保最小的 `Assets/Scenes` 结构存在
- `RefreshCurrentFolder()` 会重建目录树,同时尽量保留当前路径和当前选择
- 若扫描或文件系统操作抛异常,默认实现倾向于保留最小可工作的空根目录,而不是把异常继续上抛
## 相关文档
- [IProjectManager](IProjectManager.md)
- [File Operations](File-Operations.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md)

View File

@@ -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)

View File

@@ -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. 若找到工作区,则优先使用 `<workspace>/project`
5. 若仍找不到,再尝试从可执行目录向上查找工作区。
6. 最后回退到工作目录,若工作目录为空则回退到可执行目录。
## 工作区与项目判定
内部 helper 当前使用的规则很直接:
- `IsWorkspaceRoot(candidate)`
- 需要同时存在 `CMakeLists.txt`
- 需要有 `editor/``engine/` 目录
- `IsEditorProjectRoot(candidate)`
- 只要存在 `Project.xcproject`,或存在 `Assets/` 目录
这说明它不是通用项目发现器,而是明显面向当前仓库结构的启动辅助逻辑。
## 默认项目策略
`ResolveWorkspaceDefaultProjectRoot()` 当前优先返回:
- `<workspace>/project`
如果该目录本身已经符合项目根判定,就直接使用它;否则才尝试把工作区根本身当作项目根。
## 工作目录切换
`SetEditorWorkingDirectory(projectRootUtf8)` 会把进程当前工作目录切换到解析出来的项目根,并返回是否成功。
这保证了后续相对路径、资源加载和项目输出目录尽量围绕项目根工作,而不是围绕可执行文件目录。
## 当前实现边界
- 当前实现明显偏 Windows 桌面路径和当前仓库布局,不是跨平台项目定位库。
- 命令行解析只识别 `--project` / `-p` 两种形式。
- `FindWorkspaceRoot()` 依赖向上查找 `CMakeLists.txt + editor + engine` 组合,不适合其他目录结构。
## 相关文档
- [Core](../Core.md)
- [Application](../../Application/Application.md)
- [Win32Utf8](../../Platform/Win32Utf8/Win32Utf8.md)

View File

@@ -18,7 +18,7 @@
## 当前实现特征 ## 当前实现特征
- `ProjectManager` 更偏文件系统浏览器。 - `ProjectManager` 更偏文件系统浏览器,但当前已经包含 `.meta` sidecar 跟随、case-only rename 和项目目录初始化等项目维护语义
- `SceneManager` 更偏场景编辑服务与场景文件生命周期管理。 - `SceneManager` 更偏场景编辑服务与场景文件生命周期管理。
- `SelectionManager` 则是旧版单例实现,当前主链路已转向 `Core/ISelectionManager + Core/SelectionManager` - `SelectionManager` 则是旧版单例实现,当前主链路已转向 `Core/ISelectionManager + Core/SelectionManager`

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,41 @@
# ProjectManager::Initialize
```cpp
void Initialize(const std::string& projectPath);
```
## 当前行为
`Initialize(...)` 会把传入路径记为 `m_projectPath`,然后把 `<projectPath>/Assets` 作为当前项目浏览根目录。
按当前实现:
1. 计算 `<project>/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)

View File

@@ -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)

View File

@@ -6,35 +6,203 @@
**源文件**: `editor/src/Managers/ProjectManager.h` **源文件**: `editor/src/Managers/ProjectManager.h`
**描述**: `IProjectManager` 的默认实现,负责扫描项目 `Assets` 目录、维护面包屑路径,并支持基础文件夹与文件操作。 **描述**: `IProjectManager` 的默认实现,负责项目 `Assets` 目录扫描成 `AssetItem` 树,并提供路径导航、选择同步与基础文件操作。
## 概述 ## 概述
`ProjectManager` 当前本质上是一个面向 `Assets` 目录的轻量文件系统浏览器。 `ProjectManager` 当前不是 `AssetDatabase`,也不是资源导入服务;但它也已经不只是“轻量文件浏览器
它负责: 按当前实现,它承担三类稳定职责:
- 初始化项目目录结构 - `<Project>/Assets` 扫描为 `AssetItem` 树,供 [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md) 消费
- 扫描目录并构建 `AssetItem` - 维护浏览路径、当前选择与刷新后的状态恢复
- 维护当前路径栈 - 处理创建、删除、移动、重命名等基础文件系统操作
- 创建文件夹、删除项、移动项
## 当前实现说明 因此它更准确的定位是:
- `Initialize(projectPath)` 会确保 `<project>/Assets` 以及若干默认子目录存在。 - editor 侧项目文件系统投影层
- 若第一次初始化发现目录不存在,会自动创建 `Textures/Models/Scripts/Materials/Scenes` 等目录,并生成少量示例文件。 - Project Browser 的默认执行层
- `GetCurrentItems()` 返回当前路径末尾节点的 `children`
- 目录扫描时,文件夹会排在文件前面,并按名字排序。
- 资源类型当前按扩展名启发式推断,例如 `.png -> Texture``.fbx -> Model``.cs/.cpp/.h -> Script`
## 当前实现边界 而不是 engine 侧资产数据库本身。
- 目前是实时扫描树,不是文件系统监听模式。 ## 内部状态模型
- 异常处理大多是吞掉异常并保持当前状态,错误反馈很弱。
- 类型判断完全靠扩展名映射,不依赖资源导入数据库。 当前长期持有的核心状态只有四类:
| 状态 | 作用 |
|------|------|
| `m_rootFolder` | 当前 `Assets` 根节点。 |
| `m_path` | 当前浏览路径的目录节点栈,也是面包屑来源。 |
| `m_selectedItemPath` | 当前选中项的完整路径字符串。 |
| `m_projectPath` | 当前项目根目录。 |
这套设计的直接收益是:
- 刷新树时,不需要保留旧 `AssetItem` 指针身份
- 只要路径还能重新解析,当前目录和当前选中项就能在重建后恢复
## 扫描与类型推断
### 根目录初始化
[Initialize](Initialize.md) 当前固定把 `<project>/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) - [Managers](../Managers.md)
- [IProjectManager](../../Core/IProjectManager/IProjectManager.md) - [IProjectManager](../../Core/IProjectManager/IProjectManager.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [ProjectPanel](../../panels/ProjectPanel/ProjectPanel.md)
- [AssetItem](../../Core/AssetItem/AssetItem.md) - [AssetItem](../../Core/AssetItem/AssetItem.md)
- [AssetDatabase](../../../Core/Asset/AssetDatabase/AssetDatabase.md)

View File

@@ -0,0 +1,38 @@
# ProjectManager::RefreshCurrentFolder
```cpp
void RefreshCurrentFolder();
```
## 当前行为
这个方法当前不是“只刷新当前目录 children 列表”,而是调用内部 `RebuildTreePreservingPath()` 重建整棵 `Assets` 树,然后尽量恢复当前浏览状态。
内部主要流程是:
1. 记录旧的目录路径栈 `m_path`
2. 重新从 `<project>/Assets` 扫描整棵树
3. 重新解析旧路径栈,尽量恢复当前面包屑位置
4. 通过 `SyncSelection()` 重新校验当前 `m_selectedItemPath`
## 保留语义
- 若原浏览路径中的每一级目录仍然存在,则当前目录会被保留
- 若中间某一级目录已不存在,则恢复过程会在该层停止
- 若当前选中项路径仍能在当前目录项中重新解析,选择会保留
- 若当前选中项已不存在,则选择会被清空
这也是当前刷新后即使目录项顺序变化,选择仍可恢复的原因。
## 失败语义
当前实现把异常吞掉,因此:
- 刷新失败时不会抛异常
- 也不会提供错误返回值
- 上层看到的通常是“保持旧状态”
## 相关文档
- [ProjectManager](ProjectManager.md)
- [Initialize](Initialize.md)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -6,121 +6,263 @@
**源文件**: `editor/src/panels/ProjectPanel.h` **源文件**: `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。 - `RenderBrowserHeader(manager)`
- Body以资源网格形式显示当前目录下通过搜索过滤后的条目。 - `RenderBrowserPane(manager)` 的资源网格主体
资源卡片当前通过 `UI::DrawAssetTile(...)` 绘制,并根据 `item->isFolder` 区分: ### Breadcrumb
- `Folder` 图标 Header 当前通过 `UI::DrawToolbarBreadcrumbs(...)` 渲染路径,并消费:
- `File` 图标
交互结果统一收集到 `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) - [panels](../panels.md)
- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [IProjectManager](../../Core/IProjectManager/IProjectManager.md) - [IProjectManager](../../Core/IProjectManager/IProjectManager.md)
- [ProjectManager](../../Managers/ProjectManager/ProjectManager.md) - [ProjectManager](../../Managers/ProjectManager/ProjectManager.md)
- [AssetItem](../../Core/AssetItem/AssetItem.md) - [AssetItem](../../Core/AssetItem/AssetItem.md)
- [ProjectActionRouter](../../Actions/ProjectActionRouter/ProjectActionRouter.md) - [Widgets](../../UI/Widgets/Widgets.md)
- [ProjectCommands](../../Commands/ProjectCommands/ProjectCommands.md)
- [TreeView](../../UI/TreeView/TreeView.md)
- [BuiltInIcons](../../UI/BuiltInIcons/BuiltInIcons.md)
- [SplitterChrome](../../UI/SplitterChrome/SplitterChrome.md)

View File

@@ -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)