434 lines
16 KiB
C++
434 lines
16 KiB
C++
#include <XCEditor/Collections/UIEditorListViewInteraction.h>
|
|
|
|
#include <XCEngine/Input/InputTypes.h>
|
|
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
namespace XCEngine::UI::Editor {
|
|
|
|
namespace {
|
|
|
|
using ::XCEngine::Input::KeyCode;
|
|
using ::XCEngine::UI::UIInputEvent;
|
|
using ::XCEngine::UI::UIInputEventType;
|
|
using ::XCEngine::UI::UIPointerButton;
|
|
using Widgets::BuildUIEditorListViewLayout;
|
|
using Widgets::FindUIEditorListViewItemIndex;
|
|
using Widgets::HitTestUIEditorListView;
|
|
using Widgets::IsUIEditorListViewPointInside;
|
|
using Widgets::UIEditorListViewHitTarget;
|
|
using Widgets::UIEditorListViewHitTargetKind;
|
|
using Widgets::UIEditorListViewInvalidIndex;
|
|
|
|
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
|
switch (event.type) {
|
|
case UIInputEventType::PointerMove:
|
|
case UIInputEventType::PointerEnter:
|
|
case UIInputEventType::PointerButtonDown:
|
|
case UIInputEventType::PointerButtonUp:
|
|
case UIInputEventType::PointerWheel:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void SyncHoverTarget(
|
|
UIEditorListViewInteractionState& state,
|
|
const Widgets::UIEditorListViewLayout& layout,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items) {
|
|
state.listViewState.hoveredItemId.clear();
|
|
if (!state.hasPointerPosition) {
|
|
return;
|
|
}
|
|
|
|
const UIEditorListViewHitTarget hitTarget =
|
|
HitTestUIEditorListView(layout, state.pointerPosition);
|
|
if (hitTarget.itemIndex < items.size()) {
|
|
state.listViewState.hoveredItemId = items[hitTarget.itemIndex].itemId;
|
|
}
|
|
}
|
|
|
|
void SyncKeyboardNavigation(
|
|
UIEditorListViewInteractionState& state,
|
|
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items) {
|
|
state.keyboardNavigation.SetItemCount(items.size());
|
|
state.keyboardNavigation.ClampToItemCount();
|
|
|
|
if (!selectionModel.HasSelection()) {
|
|
return;
|
|
}
|
|
|
|
const std::size_t selectedIndex =
|
|
FindUIEditorListViewItemIndex(items, selectionModel.GetSelectedId());
|
|
if (selectedIndex == UIEditorListViewInvalidIndex) {
|
|
return;
|
|
}
|
|
|
|
if (!state.keyboardNavigation.HasCurrentIndex() ||
|
|
state.keyboardNavigation.GetCurrentIndex() != selectedIndex) {
|
|
state.keyboardNavigation.SetCurrentIndex(selectedIndex);
|
|
}
|
|
}
|
|
|
|
void SyncSelectionAnchor(
|
|
UIEditorListViewInteractionState& state,
|
|
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel) {
|
|
if (!selectionModel.HasSelection()) {
|
|
state.selectionAnchorId.clear();
|
|
return;
|
|
}
|
|
|
|
if (state.selectionAnchorId.empty() ||
|
|
!selectionModel.IsSelected(state.selectionAnchorId)) {
|
|
state.selectionAnchorId = selectionModel.GetSelectedId();
|
|
}
|
|
}
|
|
|
|
bool SelectItem(
|
|
UIEditorListViewInteractionState& state,
|
|
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items,
|
|
std::size_t itemIndex,
|
|
UIEditorListViewInteractionResult& result,
|
|
bool markKeyboardNavigation) {
|
|
if (itemIndex >= items.size()) {
|
|
return false;
|
|
}
|
|
|
|
const Widgets::UIEditorListViewItem& item = items[itemIndex];
|
|
result.selectionChanged = selectionModel.SetSelection(item.itemId);
|
|
result.selectedItemId = item.itemId;
|
|
result.selectedIndex = itemIndex;
|
|
result.keyboardNavigated = markKeyboardNavigation;
|
|
result.consumed = true;
|
|
state.keyboardNavigation.SetCurrentIndex(itemIndex);
|
|
state.selectionAnchorId = item.itemId;
|
|
return true;
|
|
}
|
|
|
|
bool ToggleItemSelection(
|
|
UIEditorListViewInteractionState& state,
|
|
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items,
|
|
std::size_t itemIndex,
|
|
UIEditorListViewInteractionResult& result) {
|
|
if (itemIndex >= items.size()) {
|
|
return false;
|
|
}
|
|
|
|
const Widgets::UIEditorListViewItem& item = items[itemIndex];
|
|
result.selectionChanged = selectionModel.ToggleSelectionMembership(item.itemId, true);
|
|
result.selectedItemId = item.itemId;
|
|
result.selectedIndex = itemIndex;
|
|
result.consumed = true;
|
|
state.keyboardNavigation.SetCurrentIndex(itemIndex);
|
|
state.selectionAnchorId = item.itemId;
|
|
return true;
|
|
}
|
|
|
|
bool SelectItemRange(
|
|
UIEditorListViewInteractionState& state,
|
|
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items,
|
|
std::size_t itemIndex,
|
|
UIEditorListViewInteractionResult& result,
|
|
bool markKeyboardNavigation) {
|
|
if (itemIndex >= items.size()) {
|
|
return false;
|
|
}
|
|
|
|
std::size_t anchorIndex = UIEditorListViewInvalidIndex;
|
|
if (!state.selectionAnchorId.empty()) {
|
|
anchorIndex = FindUIEditorListViewItemIndex(items, state.selectionAnchorId);
|
|
}
|
|
if (anchorIndex == UIEditorListViewInvalidIndex) {
|
|
return SelectItem(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
itemIndex,
|
|
result,
|
|
markKeyboardNavigation);
|
|
}
|
|
|
|
const std::size_t rangeBegin = (std::min)(anchorIndex, itemIndex);
|
|
const std::size_t rangeEnd = (std::max)(anchorIndex, itemIndex);
|
|
std::vector<std::string> selectedIds = {};
|
|
selectedIds.reserve(rangeEnd - rangeBegin + 1u);
|
|
for (std::size_t index = rangeBegin; index <= rangeEnd; ++index) {
|
|
selectedIds.push_back(items[index].itemId);
|
|
}
|
|
|
|
const Widgets::UIEditorListViewItem& item = items[itemIndex];
|
|
result.selectionChanged = selectionModel.SetSelections(std::move(selectedIds), item.itemId);
|
|
result.selectedItemId = item.itemId;
|
|
result.selectedIndex = itemIndex;
|
|
result.keyboardNavigated = markKeyboardNavigation;
|
|
result.consumed = true;
|
|
state.keyboardNavigation.SetCurrentIndex(itemIndex, false);
|
|
return true;
|
|
}
|
|
|
|
bool ApplyKeyboardNavigation(
|
|
UIEditorListViewInteractionState& state,
|
|
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items,
|
|
std::int32_t keyCode,
|
|
UIEditorListViewInteractionResult& result,
|
|
bool extendSelectionRange) {
|
|
switch (static_cast<KeyCode>(keyCode)) {
|
|
case KeyCode::Up:
|
|
if (!state.keyboardNavigation.MovePrevious()) {
|
|
return false;
|
|
}
|
|
break;
|
|
case KeyCode::Down:
|
|
if (!state.keyboardNavigation.MoveNext()) {
|
|
return false;
|
|
}
|
|
break;
|
|
case KeyCode::Home:
|
|
if (!state.keyboardNavigation.MoveHome()) {
|
|
return false;
|
|
}
|
|
break;
|
|
case KeyCode::End:
|
|
if (!state.keyboardNavigation.MoveEnd()) {
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (!state.keyboardNavigation.HasCurrentIndex()) {
|
|
return false;
|
|
}
|
|
|
|
return extendSelectionRange
|
|
? SelectItemRange(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
state.keyboardNavigation.GetCurrentIndex(),
|
|
result,
|
|
true)
|
|
: SelectItem(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
state.keyboardNavigation.GetCurrentIndex(),
|
|
result,
|
|
true);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
UIEditorListViewInteractionFrame UpdateUIEditorListViewInteraction(
|
|
UIEditorListViewInteractionState& state,
|
|
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
|
const ::XCEngine::UI::UIRect& bounds,
|
|
const std::vector<Widgets::UIEditorListViewItem>& items,
|
|
const std::vector<UIInputEvent>& inputEvents,
|
|
const Widgets::UIEditorListViewMetrics& metrics) {
|
|
Widgets::UIEditorListViewLayout layout =
|
|
BuildUIEditorListViewLayout(bounds, items, metrics);
|
|
SyncKeyboardNavigation(state, selectionModel, items);
|
|
SyncSelectionAnchor(state, selectionModel);
|
|
SyncHoverTarget(state, layout, items);
|
|
|
|
UIEditorListViewInteractionResult interactionResult = {};
|
|
for (const UIInputEvent& event : inputEvents) {
|
|
if (ShouldUsePointerPosition(event)) {
|
|
state.pointerPosition = event.position;
|
|
state.hasPointerPosition = true;
|
|
} else if (event.type == UIInputEventType::PointerLeave) {
|
|
state.hasPointerPosition = false;
|
|
}
|
|
|
|
UIEditorListViewInteractionResult eventResult = {};
|
|
switch (event.type) {
|
|
case UIInputEventType::FocusGained:
|
|
state.listViewState.focused = true;
|
|
break;
|
|
|
|
case UIInputEventType::FocusLost:
|
|
state.listViewState.focused = false;
|
|
state.hasPointerPosition = false;
|
|
state.listViewState.hoveredItemId.clear();
|
|
break;
|
|
|
|
case UIInputEventType::PointerMove:
|
|
case UIInputEventType::PointerEnter:
|
|
case UIInputEventType::PointerLeave:
|
|
break;
|
|
|
|
case UIInputEventType::PointerButtonDown: {
|
|
const UIEditorListViewHitTarget hitTarget =
|
|
state.hasPointerPosition
|
|
? HitTestUIEditorListView(layout, state.pointerPosition)
|
|
: UIEditorListViewHitTarget {};
|
|
eventResult.hitTarget = hitTarget;
|
|
|
|
const bool insideList =
|
|
state.hasPointerPosition &&
|
|
IsUIEditorListViewPointInside(layout.bounds, state.pointerPosition);
|
|
if ((event.pointerButton == UIPointerButton::Left ||
|
|
event.pointerButton == UIPointerButton::Right) &&
|
|
hitTarget.kind == UIEditorListViewHitTargetKind::Row) {
|
|
state.listViewState.focused = true;
|
|
eventResult.consumed = true;
|
|
} else if (event.pointerButton == UIPointerButton::Left && insideList) {
|
|
state.listViewState.focused = true;
|
|
eventResult.consumed = true;
|
|
} else if (event.pointerButton == UIPointerButton::Left) {
|
|
state.listViewState.focused = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case UIInputEventType::PointerButtonUp: {
|
|
const UIEditorListViewHitTarget hitTarget =
|
|
state.hasPointerPosition
|
|
? HitTestUIEditorListView(layout, state.pointerPosition)
|
|
: UIEditorListViewHitTarget {};
|
|
eventResult.hitTarget = hitTarget;
|
|
|
|
const bool insideList =
|
|
state.hasPointerPosition &&
|
|
IsUIEditorListViewPointInside(layout.bounds, state.pointerPosition);
|
|
if (hitTarget.itemIndex >= items.size()) {
|
|
if (event.pointerButton == UIPointerButton::Left && insideList) {
|
|
state.listViewState.focused = true;
|
|
eventResult.consumed = true;
|
|
} else if (event.pointerButton == UIPointerButton::Left) {
|
|
state.listViewState.focused = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
const Widgets::UIEditorListViewItem& item = items[hitTarget.itemIndex];
|
|
if (event.pointerButton == UIPointerButton::Left &&
|
|
hitTarget.kind == UIEditorListViewHitTargetKind::Row) {
|
|
if (event.modifiers.shift) {
|
|
SelectItemRange(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
hitTarget.itemIndex,
|
|
eventResult,
|
|
false);
|
|
} else if (event.modifiers.control) {
|
|
ToggleItemSelection(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
hitTarget.itemIndex,
|
|
eventResult);
|
|
} else {
|
|
SelectItem(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
hitTarget.itemIndex,
|
|
eventResult,
|
|
false);
|
|
}
|
|
state.listViewState.focused = true;
|
|
} else if (event.pointerButton == UIPointerButton::Right &&
|
|
hitTarget.kind == UIEditorListViewHitTargetKind::Row) {
|
|
if (selectionModel.IsSelected(item.itemId)) {
|
|
selectionModel.SetPrimarySelection(item.itemId);
|
|
eventResult.selectionChanged = false;
|
|
eventResult.selectedItemId = item.itemId;
|
|
eventResult.selectedIndex = hitTarget.itemIndex;
|
|
eventResult.consumed = true;
|
|
state.keyboardNavigation.SetCurrentIndex(hitTarget.itemIndex);
|
|
state.selectionAnchorId = item.itemId;
|
|
} else {
|
|
SelectItem(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
hitTarget.itemIndex,
|
|
eventResult,
|
|
false);
|
|
}
|
|
eventResult.secondaryClicked = true;
|
|
eventResult.consumed = true;
|
|
state.listViewState.focused = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case UIInputEventType::KeyDown:
|
|
if (state.listViewState.focused &&
|
|
!event.modifiers.control &&
|
|
!event.modifiers.alt &&
|
|
!event.modifiers.super) {
|
|
if (event.keyCode == static_cast<std::int32_t>(KeyCode::F2) &&
|
|
selectionModel.HasSelection()) {
|
|
const std::string& selectedItemId = selectionModel.GetSelectedId();
|
|
eventResult.renameRequested = true;
|
|
eventResult.renameItemId = selectedItemId;
|
|
eventResult.selectedItemId = selectedItemId;
|
|
eventResult.selectedIndex =
|
|
FindUIEditorListViewItemIndex(items, selectedItemId);
|
|
eventResult.consumed = true;
|
|
} else if (ApplyKeyboardNavigation(
|
|
state,
|
|
selectionModel,
|
|
items,
|
|
event.keyCode,
|
|
eventResult,
|
|
event.modifiers.shift)) {
|
|
eventResult.consumed = true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
layout = BuildUIEditorListViewLayout(bounds, items, metrics);
|
|
SyncKeyboardNavigation(state, selectionModel, items);
|
|
SyncSelectionAnchor(state, selectionModel);
|
|
SyncHoverTarget(state, layout, items);
|
|
if (eventResult.hitTarget.kind == UIEditorListViewHitTargetKind::None &&
|
|
state.hasPointerPosition) {
|
|
eventResult.hitTarget = HitTestUIEditorListView(layout, state.pointerPosition);
|
|
}
|
|
|
|
if (eventResult.consumed ||
|
|
eventResult.selectionChanged ||
|
|
eventResult.keyboardNavigated ||
|
|
eventResult.secondaryClicked ||
|
|
eventResult.renameRequested ||
|
|
eventResult.hitTarget.kind != UIEditorListViewHitTargetKind::None ||
|
|
!eventResult.selectedItemId.empty() ||
|
|
!eventResult.renameItemId.empty()) {
|
|
interactionResult = std::move(eventResult);
|
|
}
|
|
}
|
|
|
|
layout = BuildUIEditorListViewLayout(bounds, items, metrics);
|
|
SyncKeyboardNavigation(state, selectionModel, items);
|
|
SyncSelectionAnchor(state, selectionModel);
|
|
SyncHoverTarget(state, layout, items);
|
|
if (interactionResult.hitTarget.kind == UIEditorListViewHitTargetKind::None &&
|
|
state.hasPointerPosition) {
|
|
interactionResult.hitTarget = HitTestUIEditorListView(layout, state.pointerPosition);
|
|
}
|
|
|
|
return {
|
|
std::move(layout),
|
|
std::move(interactionResult)
|
|
};
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor
|