Refactor new editor boundaries and test ownership
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user