diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanel.cs b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanel.cs index a9032baf5..3e1384896 100644 --- a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanel.cs +++ b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionPanel.cs @@ -68,6 +68,7 @@ internal sealed class CollectionPanel : MonoBehaviour // instance and re-renders once, so the startup window self-heals. private bool _viewMissedNativeTypography; private bool _sourceCatalogWarmed; + private bool _stagingItemIdCopyEnabled; public void Initialize( IBppServices services, @@ -91,6 +92,7 @@ IGameDataDayTierResolver dayTierResolver _instance = this; _services = services; _config = services.Config; + _stagingItemIdCopyEnabled = CollectionStagingTools.IsEnabled(services.GameBuild.RawVersion); _nativeCardPreviewHost = nativeCardPreviewHost; _dayTierResolver = dayTierResolver; _catalog = new CollectionCatalog(cardMapProvider); @@ -458,10 +460,33 @@ out _ { var pos = mouse.position.ReadValue(); _virtualizer.PollHover(pos, _viewportBoundsPx); + TryCopyHoveredCardId(); } } } + private void TryCopyHoveredCardId() + { + if ( + !_stagingItemIdCopyEnabled + || IsTextInputFocused() + || _virtualizer == null + || !_virtualizer.TryGetHoveredCard(out var card) + ) + return; + + var keyboard = Keyboard.current; + if ( + keyboard?.cKey.wasPressedThisFrame != true + || (keyboard.leftCtrlKey.isPressed || keyboard.rightCtrlKey.isPressed) != true + ) + return; + + var templateId = card.Id.ToString("D"); + GUIUtility.systemCopyBuffer = templateId; + _view?.ShowStagingTemplateIdCopied(card.DisplayName, templateId); + } + private void HideNativeCardLayerImmediately() { _virtualizer?.Dispose(); @@ -603,7 +628,11 @@ private void EnsureView() return; } - _view = new CollectionPanelView(transform, new PanelCommands(this)); + _view = new CollectionPanelView( + transform, + new PanelCommands(this), + _stagingItemIdCopyEnabled + ); _view.GridViewportBoundsChanged += bounds => { diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/CollectionStagingTools.cs b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionStagingTools.cs new file mode 100644 index 000000000..c6293296c --- /dev/null +++ b/src/BazaarPlusPlus/Game/CollectionPanel/CollectionStagingTools.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace BazaarPlusPlus.Game.CollectionPanel; + +internal static class CollectionStagingTools +{ + internal static bool IsEnabled(string? rawGameVersion) => + rawGameVersion?.IndexOf("-staging", StringComparison.OrdinalIgnoreCase) >= 0; +} diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/Grid/CollectionGridVirtualizer.cs b/src/BazaarPlusPlus/Game/CollectionPanel/Grid/CollectionGridVirtualizer.cs index f799f1ced..bbc101d68 100644 --- a/src/BazaarPlusPlus/Game/CollectionPanel/Grid/CollectionGridVirtualizer.cs +++ b/src/BazaarPlusPlus/Game/CollectionPanel/Grid/CollectionGridVirtualizer.cs @@ -351,6 +351,22 @@ public void PollHover(Vector2 mousePixels, Rect viewportBoundsPx) } } + internal bool TryGetHoveredCard(out CollectionCardVm card) + { + if ( + _hoverDispatched + && _hoverPollIndex >= 0 + && _realized.TryGetValue(_hoverPollIndex, out var cell) + ) + { + card = cell.Vm; + return true; + } + + card = null!; + return false; + } + private void DispatchHoverOut() { // Hide the display-case highlight on every hover-out path (outside the viewport, in a diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.Tree.cs b/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.Tree.cs index 9d8745b6e..54ff19f53 100644 --- a/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.Tree.cs +++ b/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.Tree.cs @@ -71,6 +71,20 @@ private void BuildOperationRail(VisualElement parent) _subtitle = BPPSupporterAttributionRow.Create(); rail.Add(_subtitle); + if (_stagingItemIdCopyEnabled) + { + _stagingIdCopyLabel = CreateLabel( + Sizes.FontCorner, + FontStyle.Normal, + Colors.HistoryStatusText + ); + _stagingIdCopyLabel.text = StagingIdCopyHint; + _stagingIdCopyLabel.style.marginTop = UiSpacing.Xs; + _stagingIdCopyLabel.style.whiteSpace = WhiteSpace.NoWrap; + _stagingIdCopyLabel.style.overflow = Overflow.Hidden; + rail.Add(_stagingIdCopyLabel); + } + var primaryControlsRow = CreateOperationRow(UiSpacing.Md); rail.Add(primaryControlsRow); diff --git a/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.cs b/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.cs index 359802a92..0b9dbb4a8 100644 --- a/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.cs +++ b/src/BazaarPlusPlus/Game/CollectionPanel/Ui/CollectionPanelView.cs @@ -14,12 +14,14 @@ namespace BazaarPlusPlus.Game.CollectionPanel.Ui; internal sealed partial class CollectionPanelView : IDisposable { + private const string StagingIdCopyHint = "STAGING · 悬停卡牌后按 Ctrl+C 复制 template ID"; private const string SourceChipInitialsName = "bpp-source-chip-initials"; private const string TagChipIconName = "bpp-tag-chip-icon"; private const string TagChipLabelName = "bpp-tag-chip-label"; private readonly Transform _parent; private readonly ICollectionPanelCommands _commands; + private readonly bool _stagingItemIdCopyEnabled; private GameObject? _rootObject; private UIDocument? _document; @@ -29,6 +31,7 @@ internal sealed partial class CollectionPanelView : IDisposable private VisualElement? _root; private Label? _title; private VisualElement? _subtitle; + private Label? _stagingIdCopyLabel; private Label? _countLabel; private Label? _statusLabel; private Label? _disclaimerLabel; @@ -85,6 +88,7 @@ internal sealed partial class CollectionPanelView : IDisposable private bool _searchToggleHovered; private bool _searchTogglePressed; private bool _searchToggleFocused; + private int _stagingIdCopyFeedbackGeneration; private readonly Dictionary _heroChips = new(); private readonly Dictionary _heroChipIcons = new(); @@ -114,10 +118,31 @@ internal sealed partial class CollectionPanelView : IDisposable public event Action? GridViewportBoundsChanged; - public CollectionPanelView(Transform parent, ICollectionPanelCommands commands) + public CollectionPanelView( + Transform parent, + ICollectionPanelCommands commands, + bool stagingItemIdCopyEnabled = false + ) { _parent = parent ?? throw new ArgumentNullException(nameof(parent)); _commands = commands ?? throw new ArgumentNullException(nameof(commands)); + _stagingItemIdCopyEnabled = stagingItemIdCopyEnabled; + } + + internal void ShowStagingTemplateIdCopied(string displayName, string templateId) + { + if (_stagingIdCopyLabel == null) + return; + + var generation = ++_stagingIdCopyFeedbackGeneration; + _stagingIdCopyLabel.text = $"已复制 {displayName}: {templateId}"; + _stagingIdCopyLabel + .schedule.Execute(() => + { + if (generation == _stagingIdCopyFeedbackGeneration && _stagingIdCopyLabel != null) + _stagingIdCopyLabel.text = StagingIdCopyHint; + }) + .StartingIn(2500); } public void EnsureCreated() @@ -598,6 +623,7 @@ public float ReadScrollYPixels() public void Dispose() { + _stagingIdCopyFeedbackGeneration++; _controlsDragScroller?.Dispose(); _gridDragScroller?.Dispose(); if (_rootObject != null) @@ -612,6 +638,7 @@ public void Dispose() _typography = null; _titleOverlay = null; _root = null; + _stagingIdCopyLabel = null; _controlsScrollView = null; _controlsDragScroller = null; _gridDragScroller = null; diff --git a/tests/CollectionGridLayout.Tests/CollectionGridLayout.Tests.csproj b/tests/CollectionGridLayout.Tests/CollectionGridLayout.Tests.csproj index 769ea87aa..b75616d3f 100644 --- a/tests/CollectionGridLayout.Tests/CollectionGridLayout.Tests.csproj +++ b/tests/CollectionGridLayout.Tests/CollectionGridLayout.Tests.csproj @@ -11,6 +11,10 @@ +