Refactor new editor boundaries and test ownership

This commit is contained in:
2026-04-19 15:52:28 +08:00
parent dc13b56cf3
commit 93f06e84ed
279 changed files with 6349 additions and 3238 deletions

View File

@@ -1,5 +1,6 @@
#include "ProjectPanelInternal.h"
#include "Ports/SystemInteractionPort.h"
#include "Project/EditorProjectRuntime.h"
#include "State/EditorCommandFocusService.h"
@@ -8,14 +9,9 @@
#include <XCEditor/Foundation/UIEditorPanelInputFilter.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include "Internal/StringEncoding.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
#include <windows.h>
#include <shellapi.h>
#include <cstring>
#include <functional>
#include <memory>
#include <utility>
@@ -23,6 +19,7 @@
namespace XCEngine::UI::Editor::App {
using namespace ProjectPanelInternal;
using ::XCEngine::Input::KeyCode;
namespace GridDrag = XCEngine::UI::Editor::Collections::GridDragDrop;
namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop;
@@ -50,93 +47,7 @@ bool HasValidBounds(const UIRect& bounds) {
return bounds.width > 0.0f && bounds.height > 0.0f;
}
bool CopyTextToClipboard(std::string_view text) {
if (text.empty()) {
return false;
}
const std::wstring wideText =
App::Internal::Utf8ToWide(std::string(text));
const std::size_t byteCount =
(wideText.size() + 1u) * sizeof(wchar_t);
if (!OpenClipboard(nullptr)) {
return false;
}
struct ClipboardCloser final {
~ClipboardCloser() {
CloseClipboard();
}
} clipboardCloser = {};
if (!EmptyClipboard()) {
return false;
}
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, byteCount);
if (handle == nullptr) {
return false;
}
void* locked = GlobalLock(handle);
if (locked == nullptr) {
GlobalFree(handle);
return false;
}
std::memcpy(locked, wideText.c_str(), byteCount);
GlobalUnlock(handle);
if (SetClipboardData(CF_UNICODETEXT, handle) == nullptr) {
GlobalFree(handle);
return false;
}
return true;
}
bool ShowPathInExplorer(
const std::filesystem::path& path,
bool selectTarget) {
if (path.empty()) {
return false;
}
namespace fs = std::filesystem;
std::error_code errorCode = {};
const fs::path targetPath = path.lexically_normal();
if (!fs::exists(targetPath, errorCode) || errorCode) {
return false;
}
HINSTANCE result = nullptr;
if (selectTarget) {
const std::wstring parameters =
L"/select,\"" + targetPath.native() + L"\"";
const std::wstring workingDirectory =
targetPath.parent_path().native();
result = ShellExecuteW(
nullptr,
L"open",
L"explorer.exe",
parameters.c_str(),
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
SW_SHOWNORMAL);
} else {
const std::wstring workingDirectory =
targetPath.parent_path().native();
result = ShellExecuteW(
nullptr,
L"open",
targetPath.c_str(),
nullptr,
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
SW_SHOWNORMAL);
}
return reinterpret_cast<INT_PTR>(result) > 32;
}
constexpr auto kGridDoubleClickInterval = std::chrono::milliseconds(400);
Widgets::UIEditorMenuPopupItem BuildContextMenuCommandItem(
std::string itemId,
@@ -218,6 +129,11 @@ void ProjectPanel::SetCommandFocusService(
m_commandFocusService = commandFocusService;
}
void ProjectPanel::SetSystemInteractionHost(
Ports::SystemInteractionPort* systemInteractionHost) {
m_systemInteractionHost = systemInteractionHost;
}
void ProjectPanel::SetBuiltInIcons(const BuiltInIcons* icons) {
m_icons = icons;
if (EditorProjectRuntime* runtime = ResolveProjectRuntime();
@@ -241,6 +157,7 @@ void ProjectPanel::ResetInteractionState() {
m_layout = {};
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId.clear();
m_lastPrimaryClickTime = {};
m_hoveredBreadcrumbIndex = kInvalidLayoutIndex;
m_pressedBreadcrumbIndex = kInvalidLayoutIndex;
m_assetDropTargetSurface = DropTargetSurface::None;
@@ -844,7 +761,7 @@ bool ProjectPanel::HandleContextMenuEvent(const UIInputEvent& event) {
return false;
case UIInputEventType::KeyDown:
if (event.keyCode == VK_ESCAPE) {
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape)) {
CloseContextMenu();
return true;
}
@@ -956,19 +873,25 @@ void ProjectPanel::EmitSelectionClearedEvent(EventSource source) {
std::vector<UIInputEvent> ProjectPanel::BuildTreeInteractionInputEvents(
const std::vector<UIInputEvent>& inputEvents,
const UIRect& bounds,
bool allowInteraction,
bool panelActive) const {
const PanelInputContext& inputContext) const {
const std::vector<UIInputEvent> rawEvents =
FilterUIEditorPanelInputEvents(
BuildUIEditorPanelInputEvents(
bounds,
inputEvents,
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = allowInteraction,
.allowPointerInBounds = inputContext.allowInteraction,
.allowPointerWhileCaptured = HasActivePointerCapture(),
.allowKeyboardInput = panelActive,
.allowFocusEvents = panelActive || HasActivePointerCapture(),
.includePointerLeave = allowInteraction || HasActivePointerCapture()
});
.allowKeyboardInput = inputContext.hasInputFocus,
.allowFocusEvents =
inputContext.hasInputFocus ||
HasActivePointerCapture() ||
inputContext.focusGained ||
inputContext.focusLost,
.includePointerLeave =
inputContext.allowInteraction || HasActivePointerCapture()
},
inputContext.focusGained,
inputContext.focusLost);
const Widgets::UIEditorTreeViewLayout layout =
m_treeFrame.layout.bounds.width > 0.0f
@@ -1022,6 +945,9 @@ UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateAssetCommand(
if (target.subjectRelativePath.empty()) {
return BuildEvaluationResult(false, "Project has no selected item or current folder path.");
}
if (m_systemInteractionHost == nullptr) {
return BuildEvaluationResult(false, "Project system host is unavailable.");
}
return BuildEvaluationResult(
true,
@@ -1032,6 +958,9 @@ UIEditorHostCommandEvaluationResult ProjectPanel::EvaluateAssetCommand(
if (target.subjectItemId.empty() || target.subjectDisplayName.empty()) {
return BuildEvaluationResult(false, "Project has no selected item or current folder.");
}
if (m_systemInteractionHost == nullptr) {
return BuildEvaluationResult(false, "Project system host is unavailable.");
}
return BuildEvaluationResult(
true,
@@ -1065,7 +994,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand(
SyncCurrentFolderSelection();
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId = std::string(createdItemId);
m_lastPrimaryClickTimeMs = 0u;
m_lastPrimaryClickTime = {};
ResolveProjectRuntime()->SetSelection(createdItemId);
SyncAssetSelectionFromRuntime();
@@ -1152,7 +1081,8 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand(
return BuildDispatchResult(false, "Project has no selected item or current folder path.");
}
if (!CopyTextToClipboard(target.subjectRelativePath)) {
if (m_systemInteractionHost == nullptr ||
!m_systemInteractionHost->CopyTextToClipboard(target.subjectRelativePath)) {
return BuildDispatchResult(false, "Failed to copy the project path to the clipboard.");
}
@@ -1166,7 +1096,10 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchAssetCommand(
return BuildDispatchResult(false, "Project has no selected item or current folder.");
}
if (!ShowPathInExplorer(target.subjectPath, target.showInExplorerSelectTarget)) {
if (m_systemInteractionHost == nullptr ||
!m_systemInteractionHost->RevealPathInFileBrowser(
target.subjectPath,
target.showInExplorerSelectTarget)) {
return BuildDispatchResult(false, "Failed to reveal the target path in Explorer.");
}
@@ -1285,6 +1218,7 @@ UIEditorHostCommandDispatchResult ProjectPanel::DispatchEditCommand(
SyncAssetSelectionFromRuntime();
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId.clear();
m_lastPrimaryClickTime = {};
if (hadAssetSelection && !ResolveProjectRuntime()->HasSelection()) {
EmitSelectionClearedEvent(EventSource::GridPrimary);
}
@@ -1342,8 +1276,7 @@ void ProjectPanel::ClaimCommandFocus(
void ProjectPanel::Update(
const UIEditorPanelContentHostFrame& contentHostFrame,
const std::vector<UIInputEvent>& inputEvents,
bool allowInteraction,
bool panelActive) {
const PanelInputContext& inputContext) {
m_requestPointerCapture = false;
m_requestPointerRelease = false;
m_frameEvents.clear();
@@ -1385,17 +1318,24 @@ void ProjectPanel::Update(
m_visible = true;
SyncAssetSelectionFromRuntime();
const std::vector<UIInputEvent> filteredEvents =
FilterUIEditorPanelInputEvents(
BuildUIEditorPanelInputEvents(
panelState->bounds,
inputEvents,
UIEditorPanelInputFilterOptions{
.allowPointerInBounds = allowInteraction,
.allowPointerInBounds = inputContext.allowInteraction,
.allowPointerWhileCaptured = HasActivePointerCapture(),
.allowKeyboardInput = panelActive,
.allowFocusEvents = panelActive || HasActivePointerCapture(),
.includePointerLeave = allowInteraction || HasActivePointerCapture()
});
ClaimCommandFocus(filteredEvents, panelState->bounds, allowInteraction);
.allowKeyboardInput = inputContext.hasInputFocus,
.allowFocusEvents =
inputContext.hasInputFocus ||
HasActivePointerCapture() ||
inputContext.focusGained ||
inputContext.focusLost,
.includePointerLeave =
inputContext.allowInteraction || HasActivePointerCapture()
},
inputContext.focusGained,
inputContext.focusLost);
ClaimCommandFocus(filteredEvents, panelState->bounds, inputContext.allowInteraction);
m_navigationWidth = ClampNavigationWidth(m_navigationWidth, panelState->bounds.width);
m_layout = BuildLayout(panelState->bounds);
@@ -1426,8 +1366,7 @@ void ProjectPanel::Update(
BuildTreeInteractionInputEvents(
inputEvents,
panelState->bounds,
allowInteraction,
panelActive);
inputContext);
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_treeInteractionState,
m_folderSelection,
@@ -1631,7 +1570,7 @@ void ProjectPanel::Update(
ClearRenameState();
m_hoveredAssetItemId.clear();
m_lastPrimaryClickedAssetId.clear();
m_lastPrimaryClickTimeMs = 0u;
m_lastPrimaryClickTime = {};
SyncCurrentFolderSelection();
const std::string movedItemId = assetDragCallbacks.movedItemId.empty()
@@ -1759,17 +1698,15 @@ void ProjectPanel::Update(
EmitEvent(EventKind::AssetSelected, EventSource::GridPrimary, &assetEntry);
}
const std::uint64_t nowMs = GetTickCount64();
const std::uint64_t doubleClickThresholdMs =
static_cast<std::uint64_t>(GetDoubleClickTime());
const auto now = std::chrono::steady_clock::now();
const bool doubleClicked =
alreadySelected &&
m_lastPrimaryClickedAssetId == assetEntry.itemId &&
nowMs >= m_lastPrimaryClickTimeMs &&
nowMs - m_lastPrimaryClickTimeMs <= doubleClickThresholdMs;
m_lastPrimaryClickTime != std::chrono::steady_clock::time_point{} &&
now - m_lastPrimaryClickTime <= kGridDoubleClickInterval;
m_lastPrimaryClickedAssetId = assetEntry.itemId;
m_lastPrimaryClickTimeMs = nowMs;
m_lastPrimaryClickTime = now;
if (!doubleClicked) {
break;
}