1+ using Elements . Core ;
2+ using FrooxEngine ;
3+ using System . Diagnostics . CodeAnalysis ;
4+ using System . Runtime . CompilerServices ;
5+
6+ #pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
7+
8+ namespace MonkeyLoader . Resonite . UI
9+ {
10+ /// <summary>
11+ /// Contains extension methods to add pagination to and check for pagination on <see cref="ContextMenu"/>s.
12+ /// </summary>
13+ public static class ContextMenuPaginationExtensions
14+ {
15+ private static readonly ConditionalWeakTable < ContextMenu , PaginationInfo > _paginationInfosByContextMenu = [ ] ;
16+
17+ /// <inheritdoc cref="AddPagination(ContextMenu, int, int, out ContextMenuItem, out ContextMenuItem)"/>
18+ public static void AddPagination ( this ContextMenu contextMenu , int maxItems , int currentPage = 0 )
19+ => contextMenu . AddPagination ( maxItems , currentPage , out _ , out _ ) ;
20+
21+ /// <summary>
22+ /// Adds pagination with the given configuration to this context menu.<br/>
23+ /// If there already is pagination, it will be replaced.
24+ /// </summary>
25+ /// <remarks>
26+ /// The maximum number of non-pagination items to show per page does not include the paging buttons.<br/>
27+ /// This means that up to <c><paramref name="maxItems"/> + 2</c> non-pagination items will be displayed without pagination.
28+ /// </remarks>
29+ /// <param name="back">The newly created context menu item to move to the previous page.</param>
30+ /// <param name="forward">The newly created context menu item to move to the next page.</param>
31+ /// <returns/>
32+ /// <exception cref="ArgumentNullException">When this <paramref name="contextMenu"/> is <see langword="null"/>.</exception>
33+ /// <exception cref="ArgumentOutOfRangeException">When <paramref name="maxItems"/> is not greater than zero.</exception>
34+ /// <exception cref="InvalidOperationException">When adding the pagination failed for other reasons.</exception>
35+ /// <inheritdoc cref="TryAddPagination(ContextMenu, int, int, out ContextMenuItem?, out ContextMenuItem?)"/>
36+ public static void AddPagination ( this ContextMenu contextMenu , int maxItems , int currentPage ,
37+ out ContextMenuItem back , out ContextMenuItem forward )
38+ {
39+ ArgumentNullException . ThrowIfNull ( contextMenu ) ;
40+ ArgumentOutOfRangeException . ThrowIfLessThan ( maxItems , 0 ) ;
41+
42+ if ( ! contextMenu . TryAddPagination ( ref maxItems , ref currentPage , true , out back ! , out forward ! ) )
43+ throw new InvalidOperationException ( "Failed to add pagination!" ) ;
44+ }
45+
46+ /// <summary>
47+ /// Tries to add pagination with the given configuration to this context menu.<br/>
48+ /// If there already is pagination, nothing happens.
49+ /// </summary>
50+ /// <inheritdoc cref="TryAddPagination(ContextMenu, int, int, out ContextMenuItem?, out ContextMenuItem?)"/>
51+ public static bool TryAddPagination ( this ContextMenu contextMenu , int maxItems , int currentPage = 0 )
52+ => contextMenu . TryAddPagination ( ref maxItems , ref currentPage , false , out _ , out _ ) ;
53+
54+ /// <summary>
55+ /// Tries to add pagination with the given configuration to this context menu.<br/>
56+ /// If there already is pagination, the <see langword="out"/> parameters will
57+ /// be set to its <see cref="ContextMenuItem">menu items</see>.
58+ /// </summary>
59+ /// <param name="maxItems">The maximum number of non-pagination items to show per page. Must be greater than zero.</param>
60+ /// <param name="currentPage">The page that should be shown from the start. Can be any integer value, even negative.</param>
61+ /// <inheritdoc cref="TryAddPagination(ContextMenu, ref int, ref int, bool, out ContextMenuItem?, out ContextMenuItem?)"/>
62+ public static bool TryAddPagination ( this ContextMenu contextMenu , int maxItems , int currentPage ,
63+ [ NotNullWhen ( true ) ] out ContextMenuItem ? back , [ NotNullWhen ( true ) ] out ContextMenuItem ? forward )
64+ => contextMenu . TryAddPagination ( ref maxItems , ref currentPage , false , out back , out forward ) ;
65+
66+ /// <summary>
67+ /// Tries to add pagination with the given configuration to this context menu.<br/>
68+ /// If <paramref name="forceNew"/> is not <see langword="true"/> and there already is pagination,
69+ /// the <see langword="ref"/> and <see langword="out"/> parameters will be set to its
70+ /// configuration and <see cref="ContextMenuItem">menu items</see>.
71+ /// </summary>
72+ /// <remarks><para>
73+ /// This method will not throw exceptions, even if the <paramref name="contextMenu"/> is <see langword="null"/>,
74+ /// or <paramref name="maxItems"/> is set to an invalid number.<br/>
75+ /// Any <see langword="ref"/> and <see langword="out"/> parameters only have
76+ /// valid or updated values if the return value is <see langword="true"/>.
77+ /// </para><para>
78+ /// The maximum number of non-pagination items to show per page does not include the paging buttons.<br/>
79+ /// This means that up to <c><paramref name="maxItems"/> + 2</c> non-pagination items will be displayed without pagination.
80+ /// </para></remarks>
81+ /// <param name="contextMenu">The context menu to add pagination to.</param>
82+ /// <param name="maxItems">
83+ /// The maximum number of non-pagination items to show per page. Must be greater than zero.<br/>
84+ /// Will be set to the already configured value if pagination was
85+ /// already set up and <paramref name="forceNew"/> is <see langword="false"/>.
86+ /// </param>
87+ /// <param name="currentPage">
88+ /// The page that should be shown from the start. Can be any integer value, even negative.<br/>
89+ /// Will be set to the clamped value after set up or the current value if pagination was
90+ /// already set up and <paramref name="forceNew"/> is <see langword="false"/>.<br/>
91+ /// This can be <c>-1</c> if there's only up to <c><paramref name="maxItems"/> + 2</c> non-pagination items, so pagination isn't necessary.
92+ /// </param>
93+ /// <param name="forceNew">Whether to re-add pagination even if it is already present.</param>
94+ /// <param name="back">
95+ /// The newly created or already present context menu item to move to the
96+ /// previous page if the return value is <see langword="true"/>; otherwise, <see langword="null"/>.
97+ /// </param>
98+ /// <param name="forward">
99+ /// The newly created or already present context menu item to move to the
100+ /// next page if the return value is <see langword="true"/>; otherwise, <see langword="null"/>.
101+ /// </param>
102+ /// <returns><see langword="true"/> if this <see cref="ContextMenu"/> is now paginated; otherwise, <see langword="false"/>.</returns>
103+ public static bool TryAddPagination ( this ContextMenu contextMenu , ref int maxItems , ref int currentPage , bool forceNew ,
104+ [ NotNullWhen ( true ) ] out ContextMenuItem ? back , [ NotNullWhen ( true ) ] out ContextMenuItem ? forward )
105+ {
106+ if ( contextMenu . HasPagination ( out var paginationInfo ) )
107+ {
108+ if ( ! forceNew )
109+ {
110+ maxItems = paginationInfo . MaxItems ;
111+ currentPage = paginationInfo . CurrentPage ;
112+
113+ back = paginationInfo . Back ;
114+ forward = paginationInfo . Forward ;
115+
116+ return true ;
117+ }
118+
119+ paginationInfo . Destroy ( ) ;
120+ }
121+
122+ back = null ;
123+ forward = null ;
124+
125+ if ( contextMenu is null || maxItems <= 0 )
126+ return false ;
127+
128+ if ( contextMenu . LocalUser != contextMenu . Owner . Target || contextMenu . _ui is null )
129+ return false ;
130+
131+ paginationInfo = new ( contextMenu , maxItems , currentPage ) ;
132+
133+ back = paginationInfo . Back ;
134+ forward = paginationInfo . Forward ;
135+ currentPage = paginationInfo . CurrentPage ;
136+
137+ return true ;
138+ }
139+
140+ /// <summary>
141+ /// Checks if this context menu already has pagination.
142+ /// </summary>
143+ /// <inheritdoc cref="HasPagination(ContextMenu, out int, out int, out ContextMenuItem?, out ContextMenuItem?)"/>
144+ public static bool HasPagination ( this ContextMenu contextMenu )
145+ => contextMenu . HasPagination ( out _ ) ;
146+
147+ /// <summary>
148+ /// Checks if this context menu already has pagination and returns its configuration if so.
149+ /// </summary>
150+ /// <inheritdoc cref="HasPagination(ContextMenu, out int, out int, out ContextMenuItem?, out ContextMenuItem?)"/>
151+ public static bool HasPagination ( this ContextMenu contextMenu , out int maxItems , out int currentPage )
152+ => contextMenu . HasPagination ( out maxItems , out currentPage , out _ , out _ ) ;
153+
154+ /// <summary>
155+ /// Checks if this context menu already has pagination and returns its
156+ /// configuration and <see cref="ContextMenuItem">menu items</see> if so.
157+ /// </summary>
158+ /// <param name="contextMenu">The context menu to check for pagination.</param>
159+ /// <param name="maxItems">The maximum number of non-pagination items shown per page if there is pagination; otherwise, <see cref="int.MinValue"/>.</param>
160+ /// <param name="currentPage">
161+ /// The currently shown page if there is pagination; otherwise, <see cref="int.MinValue"/>.<br/>
162+ /// This can be <c>-1</c> if there's only up to <c><paramref name="maxItems"/> + 2</c> non-pagination items, so pagination isn't necessary.
163+ /// </param>
164+ /// <param name="back">The context menu item to move to the previous page if there is pagination; otherwise, <see langword="null"/>.</param>
165+ /// <param name="forward">The context menu item to move to the next page if there is pagination; otherwise, <see langword="null"/>.</param>
166+ /// <returns><see langword="true"/> if there is pagination on this context menu; otherwise, <see langword="false"/>.</returns>
167+ public static bool HasPagination ( this ContextMenu contextMenu , out int maxItems , out int currentPage ,
168+ [ NotNullWhen ( true ) ] out ContextMenuItem ? back , [ NotNullWhen ( true ) ] out ContextMenuItem ? forward )
169+ {
170+ if ( ! contextMenu . HasPagination ( out var paginationInfo ) )
171+ {
172+ maxItems = int . MinValue ;
173+ currentPage = int . MinValue ;
174+ back = null ;
175+ forward = null ;
176+
177+ return false ;
178+ }
179+
180+ maxItems = paginationInfo . MaxItems ;
181+ currentPage = paginationInfo . CurrentPage ;
182+ back = paginationInfo . Back ;
183+ forward = paginationInfo . Forward ;
184+
185+ return true ;
186+ }
187+
188+ private static bool HasPagination ( this ContextMenu contextMenu , [ NotNullWhen ( true ) ] out PaginationInfo ? paginationInfo )
189+ {
190+ if ( ! _paginationInfosByContextMenu . TryGetValue ( contextMenu , out paginationInfo ) )
191+ return false ;
192+
193+ if ( paginationInfo . IsInvalid )
194+ paginationInfo = null ;
195+
196+ return paginationInfo is not null ;
197+ }
198+
199+ private sealed class PaginationInfo
200+ {
201+ public ContextMenuItem Back { get ; }
202+ public ContextMenu ContextMenu { get ; }
203+ public int CurrentPage { get ; private set ; }
204+ public ContextMenuItem Forward { get ; }
205+ public int MaxItems { get ; }
206+
207+ public bool IsInvalid => ContextMenu . FilterWorldElement ( ) is null
208+ || Back . FilterWorldElement ( ) is null || Forward . FilterWorldElement ( ) is null ;
209+
210+ public PaginationInfo ( ContextMenu contextMenu , int maxItems , int currentPage )
211+ {
212+ ContextMenu = contextMenu ;
213+ MaxItems = maxItems ;
214+ CurrentPage = currentPage ;
215+
216+ Back = contextMenu . AddItem ( "Back" , OfficialAssets . Common . Icons . Left_Chevron , RadiantUI_Constants . Neutrals . LIGHT ) ;
217+ Back . Slot . OrderOffset = long . MinValue ;
218+ Back . Button . LocalPressed += PreviousPage ;
219+ Back . Destroyed += Destroy ;
220+
221+ Forward = contextMenu . AddItem ( "Forward" , OfficialAssets . Common . Icons . Right_Chevron , RadiantUI_Constants . Neutrals . LIGHT ) ;
222+ Forward . Slot . OrderOffset = long . MaxValue ;
223+ Forward . Button . LocalPressed += NextPage ;
224+
225+ var itemsRoot = ContextMenu . _itemsRoot . Target ;
226+ itemsRoot . ChildAdded += SlotChildrenChanged ;
227+ itemsRoot . ChildRemoved += SlotChildrenChanged ;
228+ itemsRoot . ChildrenOrderInvalidated += SlotChildrenChanged ;
229+
230+ UpdatePagination ( ) ;
231+
232+ _paginationInfosByContextMenu . AddOrUpdate ( contextMenu , this ) ;
233+ }
234+
235+ public void Destroy ( IDestroyable ? destroyable = null )
236+ {
237+ var itemsRoot = ContextMenu . _itemsRoot . Target ;
238+
239+ itemsRoot . ChildAdded -= SlotChildrenChanged ;
240+ itemsRoot . ChildRemoved -= SlotChildrenChanged ;
241+ itemsRoot . ChildrenOrderInvalidated -= SlotChildrenChanged ;
242+
243+ _paginationInfosByContextMenu . Remove ( ContextMenu ) ;
244+ }
245+
246+ private void NextPage ( IButton button , ButtonEventData args )
247+ {
248+ ++ CurrentPage ;
249+ UpdatePagination ( ) ;
250+ }
251+
252+ private void PreviousPage ( IButton button , ButtonEventData args )
253+ {
254+ -- CurrentPage ;
255+ UpdatePagination ( ) ;
256+ }
257+
258+ private void SlotChildrenChanged ( Slot slot , Slot child )
259+ => UpdatePagination ( ) ;
260+
261+ private void SlotChildrenChanged ( Slot slot )
262+ => UpdatePagination ( ) ;
263+
264+ private void UpdatePagination ( )
265+ {
266+ if ( IsInvalid )
267+ return ;
268+
269+ // Do not count the paging buttons
270+ var itemsRoot = ContextMenu . _itemsRoot . Target ;
271+ var items = itemsRoot . ChildrenCount - 2 ;
272+
273+ if ( items <= MaxItems )
274+ {
275+ CurrentPage = - 1 ;
276+ Back . Slot . ActiveSelf = false ;
277+ Forward . Slot . ActiveSelf = false ;
278+ return ;
279+ }
280+
281+ // Shift max page down by one item to prevent empty page at the end
282+ var maxPage = ( items - 1 ) / MaxItems ;
283+ CurrentPage = MathX . Max ( 0 , MathX . Min ( maxPage , CurrentPage ) ) ;
284+
285+ Back . Slot . ActiveSelf = true ;
286+ Back . Button . Enabled = CurrentPage > 0 ;
287+
288+ Forward . Slot . ActiveSelf = true ;
289+ Forward . Button . Enabled = CurrentPage < maxPage ;
290+
291+ var pageStart = CurrentPage * MaxItems ;
292+ var pageEnd = ( CurrentPage + 1 ) * MaxItems ;
293+
294+ for ( var i = 0 ; i < items ; ++ i )
295+ itemsRoot [ i + 1 ] . ActiveSelf = i >= pageStart && i < pageEnd ;
296+ }
297+ }
298+ }
299+ }
300+
301+ #pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
0 commit comments