fix(ui): select retained items on mouse down
Port UIElement_ListBox's press-time selection ordering through the shared retained item-list contract. Inventory, loot, paperdoll, and physical shortcuts now update canonical selection before release or drag promotion, while target-mode consumption suppresses drag and release-time activation. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
2dd5cb80d2
commit
043ab10b3c
18 changed files with 292 additions and 62 deletions
|
|
@ -83,6 +83,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
ConfigureList(_topContainer, ContainerCellSize, horizontalScroll: false, containerEmptySprite);
|
||||
ConfigureList(_containerList, ContainerCellSize, horizontalScroll: true, containerEmptySprite);
|
||||
ConfigureList(_contentsList, ItemCellSize, horizontalScroll: true, contentsEmptySprite);
|
||||
_topContainer.PrimaryItemPressed = PressItem;
|
||||
_containerList.PrimaryItemPressed = PressItem;
|
||||
_contentsList.PrimaryItemPressed = PressItem;
|
||||
_topContainer.ExamineItemRequested = ExamineItem;
|
||||
_containerList.ExamineItemRequested = ExamineItem;
|
||||
_contentsList.ExamineItemRequested = ExamineItem;
|
||||
|
|
@ -325,7 +328,6 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
private void AddRootCell(uint guid)
|
||||
{
|
||||
UiItemSlot cell = CreateCell(_topContainer, guid, ItemDragSource.Ground);
|
||||
cell.Clicked = () => HandlePrimaryClick(guid, () => Select(guid));
|
||||
cell.DoubleClicked = RequestClose;
|
||||
cell.IsOpenContainer = _openContainer == guid;
|
||||
SetCapacity(cell, guid);
|
||||
|
|
@ -335,7 +337,7 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
private void AddContainerCell(uint guid)
|
||||
{
|
||||
UiItemSlot cell = CreateCell(_containerList, guid, ItemDragSource.Ground);
|
||||
cell.Clicked = () => HandlePrimaryClick(guid, () => OpenNestedContainer(guid));
|
||||
cell.Clicked = () => OpenNestedContainer(guid);
|
||||
SetCapacity(cell, guid);
|
||||
_containerList.AddItem(cell);
|
||||
}
|
||||
|
|
@ -343,7 +345,6 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
private void AddContentsCell(uint guid)
|
||||
{
|
||||
UiItemSlot cell = CreateCell(_contentsList, guid, ItemDragSource.Ground);
|
||||
cell.Clicked = () => HandlePrimaryClick(guid, () => Select(guid));
|
||||
cell.DoubleClicked = () => _itemInteraction.ActivateItem(guid);
|
||||
cell.DragAcceptSprite = 0x060011F9u;
|
||||
cell.DragRejectSprite = 0x060011F8u;
|
||||
|
|
@ -384,11 +385,12 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
_itemInteraction.ExamineSelectedOrEnterMode(guid);
|
||||
}
|
||||
|
||||
private void HandlePrimaryClick(uint guid, Action fallback)
|
||||
private bool PressItem(uint guid)
|
||||
{
|
||||
if (_itemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive)
|
||||
return;
|
||||
fallback();
|
||||
return true;
|
||||
Select(guid);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyIndicators()
|
||||
|
|
@ -573,6 +575,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
_objects.Cleared -= OnObjectsCleared;
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||
_topContainer.PrimaryItemPressed = null;
|
||||
_containerList.PrimaryItemPressed = null;
|
||||
_contentsList.PrimaryItemPressed = null;
|
||||
_topContainer.ExamineItemRequested = null;
|
||||
_containerList.ExamineItemRequested = null;
|
||||
_contentsList.ExamineItemRequested = null;
|
||||
|
|
|
|||
|
|
@ -153,9 +153,21 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_contentsGrid?.RegisterDragHandler(this);
|
||||
_containerList?.RegisterDragHandler(this);
|
||||
_topContainer?.RegisterDragHandler(this);
|
||||
if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = ExamineItem;
|
||||
if (_containerList is not null) _containerList.ExamineItemRequested = ExamineItem;
|
||||
if (_topContainer is not null) _topContainer.ExamineItemRequested = ExamineItem;
|
||||
if (_contentsGrid is not null)
|
||||
{
|
||||
_contentsGrid.PrimaryItemPressed = PressItem;
|
||||
_contentsGrid.ExamineItemRequested = ExamineItem;
|
||||
}
|
||||
if (_containerList is not null)
|
||||
{
|
||||
_containerList.PrimaryItemPressed = PressItem;
|
||||
_containerList.ExamineItemRequested = ExamineItem;
|
||||
}
|
||||
if (_topContainer is not null)
|
||||
{
|
||||
_topContainer.PrimaryItemPressed = PressSelfItem;
|
||||
_topContainer.ExamineItemRequested = ExamineItem;
|
||||
}
|
||||
|
||||
// Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
|
||||
_burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter;
|
||||
|
|
@ -432,12 +444,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
dragIconTexture: _dragIconIds?.Invoke(
|
||||
ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u);
|
||||
main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u;
|
||||
main.Clicked = () =>
|
||||
{
|
||||
if (_itemInteraction?.OfferSelfPrimaryClick()
|
||||
is not null and not ItemPrimaryClickResult.NotActive) return;
|
||||
OpenContainer(p);
|
||||
};
|
||||
main.Clicked = () => OpenContainer(p);
|
||||
main.DoubleClicked = () => _itemInteraction?.ActivateItem(p);
|
||||
SetCapacityBar(main, p); // main-pack fullness (items / ItemsCapacity)
|
||||
_topContainer.AddItem(main);
|
||||
|
|
@ -480,22 +487,28 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
cell.DoubleClicked = () => _itemInteraction?.ActivateItem(guid);
|
||||
if (isContainer)
|
||||
{
|
||||
cell.Clicked = () => HandlePrimaryClick(guid, () => OpenContainer(guid));
|
||||
cell.Clicked = () => OpenContainer(guid);
|
||||
SetCapacityBar(cell, guid);
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.Clicked = () => HandlePrimaryClick(guid, () => SelectItem(guid));
|
||||
}
|
||||
list.AddItem(cell);
|
||||
}
|
||||
|
||||
private void HandlePrimaryClick(uint guid, Action fallback)
|
||||
private bool PressItem(uint guid)
|
||||
{
|
||||
if (_itemInteraction?.OfferPrimaryClick(guid)
|
||||
is not null and not ItemPrimaryClickResult.NotActive)
|
||||
return;
|
||||
fallback();
|
||||
return true;
|
||||
SelectItem(guid);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool PressSelfItem(uint guid)
|
||||
{
|
||||
if (_itemInteraction?.OfferSelfPrimaryClick()
|
||||
is not null and not ItemPrimaryClickResult.NotActive)
|
||||
return true;
|
||||
SelectItem(guid);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddEmptyCell(UiItemList list)
|
||||
|
|
@ -931,9 +944,21 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.ObjectUpdated -= OnObjectChanged;
|
||||
_objects.Cleared -= OnObjectsCleared;
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
if (_contentsGrid is not null) _contentsGrid.ExamineItemRequested = null;
|
||||
if (_containerList is not null) _containerList.ExamineItemRequested = null;
|
||||
if (_topContainer is not null) _topContainer.ExamineItemRequested = null;
|
||||
if (_contentsGrid is not null)
|
||||
{
|
||||
_contentsGrid.PrimaryItemPressed = null;
|
||||
_contentsGrid.ExamineItemRequested = null;
|
||||
}
|
||||
if (_containerList is not null)
|
||||
{
|
||||
_containerList.PrimaryItemPressed = null;
|
||||
_containerList.ExamineItemRequested = null;
|
||||
}
|
||||
if (_topContainer is not null)
|
||||
{
|
||||
_topContainer.PrimaryItemPressed = null;
|
||||
_topContainer.ExamineItemRequested = null;
|
||||
}
|
||||
if (_itemInteraction is not null)
|
||||
{
|
||||
_itemInteraction.StateChanged -= OnInteractionStateChanged;
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
var (element, mask, _, unlockBit) = PaperdollSlotBackgrounds.Definitions[i];
|
||||
if (layout.FindElement(element) is not UiItemList list) continue;
|
||||
list.RegisterDragHandler(this);
|
||||
list.PrimaryItemPressed = PressItem;
|
||||
list.ExamineItemRequested = ExamineItem;
|
||||
list.Cell.SourceKind = ItemDragSource.Equipment;
|
||||
list.Cell.SlotIndex = i; // definition position = equipped drag-payload SourceSlot
|
||||
|
|
@ -91,15 +92,6 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
&& emptySlotSprites.TryGetValue(element, out uint authoredSprite)
|
||||
? authoredSprite
|
||||
: emptySlotSprite;
|
||||
list.Cell.Clicked = () =>
|
||||
{
|
||||
uint itemId = list.Cell.ItemId;
|
||||
if (itemId != 0 && _itemInteraction?.OfferPrimaryClick(itemId)
|
||||
is not null and not ItemPrimaryClickResult.NotActive)
|
||||
return;
|
||||
if (itemId != 0)
|
||||
_selection.Select(itemId, SelectionChangeSource.Paperdoll);
|
||||
};
|
||||
list.Cell.DoubleClicked = () =>
|
||||
{
|
||||
if (list.Cell.ItemId != 0)
|
||||
|
|
@ -357,6 +349,14 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
_itemInteraction.ExamineSelectedOrEnterMode(itemId);
|
||||
}
|
||||
|
||||
private bool PressItem(uint itemId)
|
||||
{
|
||||
if (_itemInteraction.OfferPrimaryClick(itemId) != ItemPrimaryClickResult.NotActive)
|
||||
return true;
|
||||
_selection.Select(itemId, SelectionChangeSource.Paperdoll);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Detach event handlers (idempotent).</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -369,7 +369,10 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.Cleared -= OnObjectsCleared;
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
foreach (var (_, list) in _slots)
|
||||
{
|
||||
list.PrimaryItemPressed = null;
|
||||
list.ExamineItemRequested = null;
|
||||
}
|
||||
if (_dollViewport is UiViewport doll)
|
||||
doll.ClickedAt = null;
|
||||
switch (_dollDragMask)
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
if (_slots[i] is { } list)
|
||||
{
|
||||
WireClick(list);
|
||||
list.PrimaryItemPressed = PressItem;
|
||||
list.ExamineItemRequested = ExamineItem;
|
||||
// B.1 drag-drop spine: this controller is the drop handler for every
|
||||
// toolbar slot list; each cell knows its slot index + that it's a
|
||||
|
|
@ -532,6 +533,19 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
};
|
||||
}
|
||||
|
||||
private bool PressItem(uint itemId)
|
||||
{
|
||||
if (_itemInteraction?.OfferPrimaryClick(itemId)
|
||||
is not null and not ItemPrimaryClickResult.NotActive)
|
||||
return true;
|
||||
|
||||
if (_selectItem is not null)
|
||||
_selectItem(itemId);
|
||||
else
|
||||
_selection?.Select(itemId, SelectionChangeSource.Toolbar);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ExamineItem(uint itemId)
|
||||
{
|
||||
if (_selectItem is not null)
|
||||
|
|
@ -777,7 +791,10 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_selection.Changed -= OnSelectionChanged;
|
||||
foreach (UiItemList? list in _slots)
|
||||
if (list is not null)
|
||||
{
|
||||
list.PrimaryItemPressed = null;
|
||||
list.ExamineItemRequested = null;
|
||||
}
|
||||
_repo.ObjectAdded -= OnRepositoryObjectChanged;
|
||||
_repo.ObjectUpdated -= OnRepositoryObjectChanged;
|
||||
_repo.ObjectRemoved -= OnRepositoryObjectChanged;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,20 @@ public sealed class UiItemList : UiElement
|
|||
/// </summary>
|
||||
public Action<object, int, int>? CatalogDropped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Physical-item left-button-down command supplied by the owning panel.
|
||||
/// Return <see langword="true"/> when a target mode consumed the press so
|
||||
/// the slot suppresses drag promotion and the completed Click/DoubleClick
|
||||
/// gesture.
|
||||
///
|
||||
/// Retail references:
|
||||
/// <c>UIElement_ListBox::MouseDown @ 0x0046E3A0</c> selects the item under
|
||||
/// the pointer during MouseDown, before optional drag selection;
|
||||
/// <c>UIElement_ItemList::ListenToElementMessage @ 0x004E4D50</c> handles
|
||||
/// target mode before ordinary physical-item selection.
|
||||
/// </summary>
|
||||
public Func<uint, bool>? PrimaryItemPressed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Appraisal command supplied by the owning panel controller for physical
|
||||
/// item cells. Retail <c>UIElement_ItemList::ListenToElementMessage
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ public class UiItemSlot : UiElement
|
|||
|
||||
private bool _waiting;
|
||||
internal bool WaitingVisual => _waiting;
|
||||
private bool _primaryPressConsumed;
|
||||
|
||||
/// <summary>RenderSurface id -> (GL texture, w, h). Set by the factory/controller.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
|
@ -134,11 +135,14 @@ public class UiItemSlot : UiElement
|
|||
DragIconTexture = 0;
|
||||
Shortcut = null;
|
||||
_waiting = false;
|
||||
_primaryPressConsumed = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? GetDragPayload()
|
||||
=> ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut) : null;
|
||||
=> ItemId != 0 && !_primaryPressConsumed
|
||||
? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut)
|
||||
: null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override (uint tex, int w, int h)? GetDragGhost()
|
||||
|
|
@ -251,8 +255,10 @@ public class UiItemSlot : UiElement
|
|||
|
||||
// ── Events / draw ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Invoked by <see cref="OnEvent"/> when a left-button-down lands on
|
||||
/// a bound slot. Wired by <c>ToolbarController</c> to the use-item callback.</summary>
|
||||
/// <summary>Invoked by <see cref="OnEvent"/> when a completed left click lands
|
||||
/// on a bound slot. Selection already occurred on MouseDown through the owning
|
||||
/// <see cref="UiItemList"/>; this callback performs release-time activation
|
||||
/// such as opening a bag or using a shortcut.</summary>
|
||||
public Action? Clicked { get; set; }
|
||||
|
||||
/// <summary>Invoked by <see cref="OnEvent"/> when a double-click lands on a bound slot.</summary>
|
||||
|
|
@ -267,12 +273,21 @@ public class UiItemSlot : UiElement
|
|||
// drag (press + >3px move) does NOT also use the item. UiRoot suppresses the
|
||||
// post-drag Click (UiRoot.cs FinishDrag returns before the Click emit).
|
||||
case UiEventType.MouseDown:
|
||||
return true; // consume the press; no use here
|
||||
// UIElement_ListBox::MouseDown @ 0x0046E3A0 selects immediately,
|
||||
// before StartDragSelect. UIElement_ItemList's notice handler
|
||||
// intercepts target mode first. Activation remains release-time,
|
||||
// so a press promoted to drag never also uses the item.
|
||||
_primaryPressConsumed = ItemId != 0
|
||||
&& FindList() is { PrimaryItemPressed: { } pressed }
|
||||
&& pressed(ItemId);
|
||||
return true;
|
||||
case UiEventType.Click:
|
||||
Clicked?.Invoke();
|
||||
if (!_primaryPressConsumed)
|
||||
Clicked?.Invoke();
|
||||
return true;
|
||||
case UiEventType.DoubleClick:
|
||||
DoubleClicked?.Invoke();
|
||||
if (!_primaryPressConsumed)
|
||||
DoubleClicked?.Invoke();
|
||||
return true;
|
||||
case UiEventType.RightClick:
|
||||
if (ItemId != 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue