A game framework written with osu! in mind.

Merge branch 'master' into tabcontrol-disabled-bindable

authored by

Dean Herbert and committed by
GitHub
28f3c209 72377e3f

+519 -19
+24
osu.Framework.Tests/Containers/TestSceneContainerState.cs
··· 272 272 AddAssert("correct count", () => count == 2); 273 273 } 274 274 275 + [Test] 276 + public void TestAliveChildrenContainsOnlyAliveChildren() 277 + { 278 + Container container = null; 279 + Drawable aliveChild = null; 280 + Drawable nonAliveChild = null; 281 + 282 + AddStep("create container", () => 283 + { 284 + Child = container = new Container 285 + { 286 + Children = new[] 287 + { 288 + aliveChild = new Box(), 289 + nonAliveChild = new Box { LifetimeStart = double.MaxValue } 290 + } 291 + }; 292 + }); 293 + 294 + AddAssert("1 alive child", () => container.AliveChildren.Count == 1); 295 + AddAssert("alive child contained", () => container.AliveChildren.Contains(aliveChild)); 296 + AddAssert("non-alive child not contained", () => !container.AliveChildren.Contains(nonAliveChild)); 297 + } 298 + 275 299 private class TestContainer : Container 276 300 { 277 301 public new void ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay);
+219 -9
osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs
··· 6 6 using System.Linq; 7 7 using NUnit.Framework; 8 8 using osu.Framework.Bindables; 9 + using osu.Framework.Graphics; 9 10 using osu.Framework.Graphics.UserInterface; 11 + using osu.Framework.Input; 10 12 using osu.Framework.Input.Events; 13 + using osu.Framework.Input.States; 11 14 using osu.Framework.Testing; 12 15 using osuTK; 13 16 using osuTK.Input; ··· 19 22 private const int items_to_add = 10; 20 23 private const float explicit_height = 100; 21 24 private float calculatedHeight; 22 - private readonly TestDropdown testDropdown, testDropdownMenu, bindableDropdown; 25 + private readonly TestDropdown testDropdown, testDropdownMenu, bindableDropdown, emptyDropdown; 26 + private readonly PlatformActionContainer platformActionContainerKeyboardSelection, platformActionContainerKeyboardPreselection, platformActionContainerEmptyDropdown; 23 27 private readonly BindableList<string> bindableList = new BindableList<string>(); 24 28 29 + private int previousIndex; 30 + private int lastVisibleIndexOnTheCurrentPage, lastVisibleIndexOnTheNextPage; 31 + private int firstVisibleIndexOnTheCurrentPage, firstVisibleIndexOnThePreviousPage; 32 + 25 33 public override IReadOnlyList<Type> RequiredTypes => new[] 26 34 { 27 35 typeof(Dropdown<>), ··· 41 49 while (i < items_to_add) 42 50 testItems[i] = @"test " + i++; 43 51 44 - Add(testDropdown = new TestDropdown 52 + Add(platformActionContainerKeyboardSelection = new PlatformActionContainer 45 53 { 46 - Width = 150, 47 - Position = new Vector2(200, 70), 48 - Items = testItems 54 + Child = testDropdown = new TestDropdown 55 + { 56 + Width = 150, 57 + Position = new Vector2(200, 70), 58 + Items = testItems 59 + } 49 60 }); 50 61 51 - Add(testDropdownMenu = new TestDropdown 62 + Add(platformActionContainerKeyboardPreselection = new PlatformActionContainer 52 63 { 53 - Width = 150, 54 - Position = new Vector2(400, 70), 55 - Items = testItems 64 + Child = testDropdownMenu = new TestDropdown 65 + { 66 + Width = 150, 67 + Position = new Vector2(400, 70), 68 + Items = testItems 69 + } 56 70 }); 71 + testDropdownMenu.Menu.MaxHeight = explicit_height; 57 72 58 73 Add(bindableDropdown = new TestDropdown 59 74 { 60 75 Width = 150, 61 76 Position = new Vector2(600, 70), 62 77 ItemSource = bindableList 78 + }); 79 + 80 + Add(platformActionContainerEmptyDropdown = new PlatformActionContainer 81 + { 82 + Child = emptyDropdown = new TestDropdown 83 + { 84 + Width = 150, 85 + Position = new Vector2(800, 70), 86 + } 63 87 }); 64 88 } 65 89 ··· 121 145 AddAssert("current value should be two", () => bindableDropdown.Current.Value == "two"); 122 146 } 123 147 148 + private void performKeypress(Drawable drawable, Key key) 149 + { 150 + drawable.TriggerEvent(new KeyDownEvent(new InputState(), key)); 151 + drawable.TriggerEvent(new KeyUpEvent(new InputState(), key)); 152 + } 153 + 154 + private void performPlatformAction(PlatformAction action, PlatformActionContainer platformActionContainer, Drawable drawable) 155 + { 156 + var tempIsHovered = drawable.IsHovered; 157 + var tempHasFocus = drawable.HasFocus; 158 + 159 + drawable.IsHovered = true; 160 + drawable.HasFocus = true; 161 + 162 + platformActionContainer.TriggerPressed(action); 163 + platformActionContainer.TriggerReleased(action); 164 + 165 + drawable.IsHovered = tempIsHovered; 166 + drawable.HasFocus = tempHasFocus; 167 + } 168 + 169 + [Test] 170 + public void KeyboardSelection() 171 + { 172 + AddStep("Select next item", () => 173 + { 174 + previousIndex = testDropdown.SelectedIndex; 175 + performKeypress(testDropdown.Header, Key.Down); 176 + }); 177 + 178 + AddAssert("Next item is selected", () => testDropdown.SelectedIndex == previousIndex + 1); 179 + 180 + AddStep("Select previous item", () => 181 + { 182 + previousIndex = testDropdown.SelectedIndex; 183 + performKeypress(testDropdown.Header, Key.Up); 184 + }); 185 + 186 + AddAssert("Previous item is selected", () => testDropdown.SelectedIndex == previousIndex - 1); 187 + 188 + AddStep("Select last item", 189 + () => performPlatformAction(new PlatformAction(PlatformActionType.ListEnd, PlatformActionMethod.Move), platformActionContainerKeyboardSelection, testDropdown.Header)); 190 + 191 + AddAssert("Last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.Last().Item); 192 + 193 + AddStep("Select first item", 194 + () => performPlatformAction(new PlatformAction(PlatformActionType.ListStart, PlatformActionMethod.Move), platformActionContainerKeyboardSelection, testDropdown.Header)); 195 + 196 + AddAssert("First item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.First().Item); 197 + 198 + AddStep("Select next item when empty", () => performKeypress(emptyDropdown.Header, Key.Up)); 199 + 200 + AddStep("Select previous item when empty", () => performKeypress(emptyDropdown.Header, Key.Down)); 201 + 202 + AddStep("Select last item when empty", () => performKeypress(emptyDropdown.Header, Key.PageUp)); 203 + 204 + AddStep("Select first item when empty", () => performKeypress(emptyDropdown.Header, Key.PageDown)); 205 + } 206 + 207 + [Test] 208 + public void KeyboardPreselection() 209 + { 210 + clickKeyboardPreselectionDropdown(); 211 + assertDropdownIsOpen(); 212 + 213 + AddStep("Preselect next item", () => 214 + { 215 + previousIndex = testDropdownMenu.PreselectedIndex; 216 + performKeypress(testDropdownMenu.Menu, Key.Down); 217 + }); 218 + 219 + AddAssert("Next item is preselected", () => testDropdownMenu.PreselectedIndex == previousIndex + 1); 220 + 221 + AddStep("Preselect previous item", () => 222 + { 223 + previousIndex = testDropdownMenu.PreselectedIndex; 224 + performKeypress(testDropdownMenu.Menu, Key.Up); 225 + }); 226 + 227 + AddAssert("Previous item is preselected", () => testDropdownMenu.PreselectedIndex == previousIndex - 1); 228 + 229 + AddStep("Preselect last visible item", () => 230 + { 231 + lastVisibleIndexOnTheCurrentPage = testDropdownMenu.Menu.DrawableMenuItems.ToList().IndexOf(testDropdownMenu.Menu.VisibleMenuItems.Last()); 232 + performKeypress(testDropdownMenu.Menu, Key.PageDown); 233 + }); 234 + 235 + AddAssert("Last visible item preselected", () => testDropdownMenu.PreselectedIndex == lastVisibleIndexOnTheCurrentPage); 236 + 237 + AddStep("Preselect last visible item on the next page", () => 238 + { 239 + lastVisibleIndexOnTheNextPage = 240 + MathHelper.Clamp(lastVisibleIndexOnTheCurrentPage + testDropdownMenu.Menu.VisibleMenuItems.Count(), 0, testDropdownMenu.Menu.Items.Count - 1); 241 + 242 + performKeypress(testDropdownMenu.Menu, Key.PageDown); 243 + }); 244 + 245 + AddAssert("Last visible item on the next page preselected", () => testDropdownMenu.PreselectedIndex == lastVisibleIndexOnTheNextPage); 246 + 247 + AddStep("Preselect first visible item", () => 248 + { 249 + firstVisibleIndexOnTheCurrentPage = testDropdownMenu.Menu.DrawableMenuItems.ToList().IndexOf(testDropdownMenu.Menu.VisibleMenuItems.First()); 250 + performKeypress(testDropdownMenu.Menu, Key.PageUp); 251 + }); 252 + 253 + AddAssert("First visible item preselected", () => testDropdownMenu.PreselectedIndex == firstVisibleIndexOnTheCurrentPage); 254 + 255 + AddStep("Preselect first visible item on the previous page", () => 256 + { 257 + firstVisibleIndexOnThePreviousPage = MathHelper.Clamp(firstVisibleIndexOnTheCurrentPage - testDropdownMenu.Menu.VisibleMenuItems.Count(), 0, 258 + testDropdownMenu.Menu.Items.Count - 1); 259 + performKeypress(testDropdownMenu.Menu, Key.PageUp); 260 + }); 261 + 262 + AddAssert("First visible item on the previous page selected", () => testDropdownMenu.PreselectedIndex == firstVisibleIndexOnThePreviousPage); 263 + 264 + AddAssert("First item is preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.First().Item); 265 + 266 + AddStep("Preselect last item", 267 + () => performPlatformAction(new PlatformAction(PlatformActionType.ListEnd, PlatformActionMethod.Move), platformActionContainerKeyboardPreselection, testDropdownMenu)); 268 + 269 + AddAssert("Last item preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.Last().Item); 270 + 271 + AddStep("Finalize selection", () => { performKeypress(testDropdownMenu.Menu, Key.Enter); }); 272 + 273 + assertLastItemSelected(); 274 + 275 + assertDropdownIsClosed(); 276 + 277 + clickKeyboardPreselectionDropdown(); 278 + 279 + assertDropdownIsOpen(); 280 + 281 + AddStep("Preselect first item", 282 + () => performPlatformAction(new PlatformAction(PlatformActionType.ListStart, PlatformActionMethod.Move), platformActionContainerKeyboardPreselection, testDropdownMenu)); 283 + 284 + AddAssert("First item preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.First().Item); 285 + 286 + AddStep("Discard preselection", () => performKeypress(testDropdownMenu.Menu, Key.Escape)); 287 + 288 + assertDropdownIsClosed(); 289 + 290 + assertLastItemSelected(); 291 + 292 + AddStep($"Click {emptyDropdown}", () => toggleDropdownViaClick(emptyDropdown)); 293 + 294 + AddStep("Preselect next item when empty", () => 295 + { 296 + performKeypress(emptyDropdown.Menu, Key.Down); 297 + }); 298 + 299 + AddStep("Preselect previous item when empty", () => 300 + { 301 + performKeypress(emptyDropdown.Menu, Key.Up); 302 + }); 303 + 304 + AddStep("Preselect first visible item when empty", () => 305 + { 306 + performKeypress(emptyDropdown.Menu, Key.PageUp); 307 + }); 308 + 309 + AddStep("Preselect last visible item when empty", () => 310 + { 311 + performKeypress(emptyDropdown.Menu, Key.PageDown); 312 + }); 313 + 314 + AddStep("Preselect first item when empty", 315 + () => performPlatformAction(new PlatformAction(PlatformActionType.ListStart, PlatformActionMethod.Move), platformActionContainerEmptyDropdown, emptyDropdown)); 316 + 317 + AddStep("Preselect last item when empty", 318 + () => performPlatformAction(new PlatformAction(PlatformActionType.ListEnd, PlatformActionMethod.Move), platformActionContainerEmptyDropdown, emptyDropdown)); 319 + 320 + void clickKeyboardPreselectionDropdown() => AddStep("click keyboardPreselectionDropdown", () => toggleDropdownViaClick(testDropdownMenu)); 321 + 322 + void assertDropdownIsOpen() => AddAssert("dropdown is open", () => testDropdownMenu.Menu.State == MenuState.Open); 323 + 324 + void assertLastItemSelected() => AddAssert("Last item selected", () => testDropdownMenu.SelectedItem == testDropdownMenu.Menu.DrawableMenuItems.Last().Item); 325 + 326 + void assertDropdownIsClosed() => AddAssert("dropdown is closed", () => testDropdownMenu.Menu.State == MenuState.Closed); 327 + } 328 + 124 329 [Test] 125 330 public void SelectNull() 126 331 { ··· 151 356 public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)? 152 357 .TriggerEvent(new ClickEvent(GetContainingInputManager().CurrentState, MouseButton.Left)); 153 358 } 359 + 360 + internal new DropdownMenuItem<string> SelectedItem => base.SelectedItem; 361 + 362 + public int SelectedIndex => Menu.DrawableMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem); 363 + public int PreselectedIndex => Menu.DrawableMenuItems.ToList().IndexOf(Menu.PreselectedItem); 154 364 } 155 365 } 156 366 }
+21
osu.Framework/Graphics/Containers/Container.cs
··· 43 43 internalChildrenAsT = (IReadOnlyList<T>)InternalChildren; 44 44 else 45 45 internalChildrenAsT = new LazyList<Drawable, T>(InternalChildren, c => (T)c); 46 + 47 + if (typeof(T) == typeof(Drawable)) 48 + aliveInternalChildrenAsT = (IReadOnlyList<T>)AliveInternalChildren; 49 + else 50 + aliveInternalChildrenAsT = new LazyList<Drawable, T>(AliveInternalChildren, c => (T)c); 46 51 } 47 52 48 53 /// <summary> ··· 73 78 return internalChildrenAsT; 74 79 } 75 80 set => ChildrenEnumerable = value; 81 + } 82 + 83 + /// <summary> 84 + /// The publicly accessible list of alive children. Forwards to the alive children of <see cref="Content"/>. 85 + /// If <see cref="Content"/> is this container, then returns <see cref="CompositeDrawable.AliveInternalChildren"/>. 86 + /// </summary> 87 + public IReadOnlyList<T> AliveChildren 88 + { 89 + get 90 + { 91 + if (Content != this) 92 + return Content.AliveChildren; 93 + 94 + return aliveInternalChildrenAsT; 95 + } 76 96 } 77 97 78 98 /// <summary> ··· 142 162 } 143 163 144 164 private readonly IReadOnlyList<T> internalChildrenAsT; 165 + private readonly IReadOnlyList<T> aliveInternalChildrenAsT; 145 166 146 167 /// <summary> 147 168 /// The index of a given child within <see cref="Children"/>.
+191 -6
osu.Framework/Graphics/UserInterface/Dropdown.cs
··· 6 6 using System.Linq; 7 7 using osu.Framework.Bindables; 8 8 using osu.Framework.Extensions; 9 + using osu.Framework.Extensions.IEnumerableExtensions; 9 10 using osu.Framework.Graphics.Containers; 11 + using osu.Framework.Graphics.Sprites; 12 + using osu.Framework.Input; 13 + using osu.Framework.Input.Bindings; 14 + using osu.Framework.Input.Events; 15 + using osuTK; 10 16 using osuTK.Graphics; 11 - using osu.Framework.Extensions.IEnumerableExtensions; 12 - using osu.Framework.Graphics.Sprites; 17 + using osuTK.Input; 13 18 14 19 namespace osu.Framework.Graphics.UserInterface 15 20 { ··· 204 209 Menu.RelativeSizeAxes = Axes.X; 205 210 206 211 Header.Action = Menu.Toggle; 212 + Header.ChangeSelection += selectionKeyPressed; 213 + Menu.PreselectionConfirmed += preselectionConfirmed; 207 214 Current.ValueChanged += selectionChanged; 208 215 209 216 ItemSource.ItemsAdded += _ => setItems(ItemSource); 210 217 ItemSource.ItemsRemoved += _ => setItems(ItemSource); 218 + } 219 + 220 + private void preselectionConfirmed(int selectedIndex) 221 + { 222 + SelectedItem = MenuItems.ElementAt(selectedIndex); 223 + Menu.State = MenuState.Closed; 224 + } 225 + 226 + private void selectionKeyPressed(DropdownHeader.DropdownSelectionAction action) 227 + { 228 + if (!MenuItems.Any()) 229 + return; 230 + 231 + var dropdownMenuItems = MenuItems.ToList(); 232 + 233 + switch (action) 234 + { 235 + case DropdownHeader.DropdownSelectionAction.Previous: 236 + SelectedItem = dropdownMenuItems[MathHelper.Clamp(dropdownMenuItems.IndexOf(SelectedItem) - 1, 0, dropdownMenuItems.Count - 1)]; 237 + break; 238 + 239 + case DropdownHeader.DropdownSelectionAction.Next: 240 + SelectedItem = dropdownMenuItems[MathHelper.Clamp(dropdownMenuItems.IndexOf(SelectedItem) + 1, 0, dropdownMenuItems.Count - 1)]; 241 + break; 242 + 243 + case DropdownHeader.DropdownSelectionAction.First: 244 + SelectedItem = dropdownMenuItems[0]; 245 + break; 246 + 247 + case DropdownHeader.DropdownSelectionAction.Last: 248 + SelectedItem = dropdownMenuItems[^1]; 249 + break; 250 + 251 + default: 252 + throw new ArgumentException("Unexpected selection action type.", nameof(action)); 253 + } 211 254 } 212 255 213 256 protected override void LoadComplete() ··· 289 332 290 333 #region DropdownMenu 291 334 292 - public abstract class DropdownMenu : Menu 335 + public abstract class DropdownMenu : Menu, IKeyBindingHandler<PlatformAction> 293 336 { 294 337 protected DropdownMenu() 295 338 : base(Direction.Vertical) 296 339 { 340 + StateChanged += clearPreselection; 297 341 } 298 342 343 + public override void Add(MenuItem item) 344 + { 345 + base.Add(item); 346 + 347 + var drawableDropdownMenuItem = (DrawableDropdownMenuItem)ItemsContainer.Single(drawableItem => drawableItem.Item == item); 348 + drawableDropdownMenuItem.PreselectionRequested += PreselectItem; 349 + } 350 + 351 + private void clearPreselection(MenuState obj) 352 + { 353 + if (obj == MenuState.Closed) 354 + PreselectItem(null); 355 + } 356 + 357 + protected internal IEnumerable<DrawableDropdownMenuItem> DrawableMenuItems => Children.OfType<DrawableDropdownMenuItem>(); 358 + protected internal IEnumerable<DrawableDropdownMenuItem> VisibleMenuItems => DrawableMenuItems.Where(item => !item.IsMaskedAway); 359 + 360 + public DrawableDropdownMenuItem PreselectedItem => Children.OfType<DrawableDropdownMenuItem>().FirstOrDefault(c => c.IsPreSelected) 361 + ?? Children.OfType<DrawableDropdownMenuItem>().FirstOrDefault(c => c.IsSelected); 362 + 363 + public event Action<int> PreselectionConfirmed; 364 + 299 365 /// <summary> 300 366 /// Selects an item from this <see cref="DropdownMenu"/>. 301 367 /// </summary> 302 368 /// <param name="item">The item to select.</param> 303 369 public void SelectItem(DropdownMenuItem<T> item) 304 370 { 305 - Children.OfType<DrawableDropdownMenuItem>().ForEach(c => c.IsSelected = c.Item == item); 371 + Children.OfType<DrawableDropdownMenuItem>().ForEach(c => 372 + { 373 + c.IsSelected = c.Item == item; 374 + if (c.IsSelected) 375 + ContentContainer.ScrollIntoView(c); 376 + }); 306 377 } 307 378 308 379 /// <summary> ··· 322 393 /// </summary> 323 394 public bool AnyPresent => Children.Any(c => c.IsPresent); 324 395 396 + protected void PreselectItem(int index) => PreselectItem(Items[MathHelper.Clamp(index, 0, DrawableMenuItems.Count() - 1)]); 397 + 398 + /// <summary> 399 + /// Preselects an item from this <see cref="DropdownMenu"/>. 400 + /// </summary> 401 + /// <param name="item">The item to select.</param> 402 + protected void PreselectItem(MenuItem item) 403 + { 404 + Children.OfType<DrawableDropdownMenuItem>().ForEach(c => 405 + { 406 + c.IsPreSelected = c.Item == item; 407 + if (c.IsPreSelected) 408 + ContentContainer.ScrollIntoView(c); 409 + }); 410 + } 411 + 325 412 protected sealed override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => CreateDrawableDropdownMenuItem(item); 326 413 327 414 protected abstract DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item); ··· 331 418 // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 332 419 public abstract class DrawableDropdownMenuItem : DrawableMenuItem 333 420 { 421 + public event Action<DropdownMenuItem<T>> PreselectionRequested; 422 + 334 423 protected DrawableDropdownMenuItem(MenuItem item) 335 424 : base(item) 336 425 { ··· 352 441 } 353 442 } 354 443 444 + private bool preSelected; 445 + 446 + /// <summary> 447 + /// Denotes whether this menu item will be selected on <see cref="Key.Enter"/> press. 448 + /// This property is related to selecting menu items using keyboard or hovering. 449 + /// </summary> 450 + public bool IsPreSelected 451 + { 452 + get => preSelected; 453 + set 454 + { 455 + if (preSelected == value) 456 + return; 457 + 458 + preSelected = value; 459 + 460 + OnSelectChange(); 461 + } 462 + } 463 + 355 464 private Color4 backgroundColourSelected = Color4.SlateGray; 356 465 357 466 public Color4 BackgroundColourSelected ··· 387 496 388 497 protected override void UpdateBackgroundColour() 389 498 { 390 - Background.FadeColour(IsHovered ? BackgroundColourHover : IsSelected ? BackgroundColourSelected : BackgroundColour); 499 + Background.FadeColour(IsPreSelected ? BackgroundColourHover : IsSelected ? BackgroundColourSelected : BackgroundColour); 391 500 } 392 501 393 502 protected override void UpdateForegroundColour() 394 503 { 395 - Foreground.FadeColour(IsHovered ? ForegroundColourHover : IsSelected ? ForegroundColourSelected : ForegroundColour); 504 + Foreground.FadeColour(IsPreSelected ? ForegroundColourHover : IsSelected ? ForegroundColourSelected : ForegroundColour); 396 505 } 397 506 398 507 protected override void LoadComplete() ··· 401 510 Background.Colour = IsSelected ? BackgroundColourSelected : BackgroundColour; 402 511 Foreground.Colour = IsSelected ? ForegroundColourSelected : ForegroundColour; 403 512 } 513 + 514 + protected override bool OnHover(HoverEvent e) 515 + { 516 + PreselectionRequested?.Invoke(Item as DropdownMenuItem<T>); 517 + return base.OnHover(e); 518 + } 404 519 } 405 520 406 521 #endregion 522 + 523 + protected override bool OnKeyDown(KeyDownEvent e) 524 + { 525 + var drawableMenuItemsList = DrawableMenuItems.ToList(); 526 + if (!drawableMenuItemsList.Any()) 527 + return base.OnKeyDown(e); 528 + 529 + var currentPreselected = drawableMenuItemsList.FirstOrDefault(i => i.IsPreSelected) ?? drawableMenuItemsList.First(i => i.IsSelected); 530 + 531 + var targetPreselectionIndex = drawableMenuItemsList.IndexOf(currentPreselected); 532 + 533 + switch (e.Key) 534 + { 535 + case Key.Up: 536 + PreselectItem(targetPreselectionIndex - 1); 537 + return true; 538 + 539 + case Key.Down: 540 + PreselectItem(targetPreselectionIndex + 1); 541 + return true; 542 + 543 + case Key.PageUp: 544 + var firstVisibleItem = VisibleMenuItems.First(); 545 + 546 + if (currentPreselected == firstVisibleItem) 547 + PreselectItem(targetPreselectionIndex - VisibleMenuItems.Count()); 548 + else 549 + PreselectItem(drawableMenuItemsList.IndexOf(firstVisibleItem)); 550 + return true; 551 + 552 + case Key.PageDown: 553 + var lastVisibleItem = VisibleMenuItems.Last(); 554 + 555 + if (currentPreselected == lastVisibleItem) 556 + PreselectItem(targetPreselectionIndex + VisibleMenuItems.Count()); 557 + else 558 + PreselectItem(drawableMenuItemsList.IndexOf(lastVisibleItem)); 559 + return true; 560 + 561 + case Key.Enter: 562 + PreselectionConfirmed?.Invoke(targetPreselectionIndex); 563 + return true; 564 + 565 + case Key.Escape: 566 + State = MenuState.Closed; 567 + return true; 568 + 569 + default: 570 + return base.OnKeyDown(e); 571 + } 572 + } 573 + 574 + public bool OnPressed(PlatformAction action) 575 + { 576 + switch (action.ActionType) 577 + { 578 + case PlatformActionType.ListStart: 579 + PreselectItem(Items.FirstOrDefault()); 580 + return true; 581 + 582 + case PlatformActionType.ListEnd: 583 + PreselectItem(Items.LastOrDefault()); 584 + return true; 585 + 586 + default: 587 + return false; 588 + } 589 + } 590 + 591 + public bool OnReleased(PlatformAction action) => false; 407 592 } 408 593 409 594 #endregion
+55 -1
osu.Framework/Graphics/UserInterface/DropdownHeader.cs
··· 2 2 // See the LICENCE file in the repository root for full licence text. 3 3 4 4 using osuTK.Graphics; 5 + using System; 5 6 using osu.Framework.Graphics.Containers; 6 7 using osu.Framework.Graphics.Shapes; 8 + using osu.Framework.Input; 9 + using osu.Framework.Input.Bindings; 7 10 using osu.Framework.Input.Events; 11 + using osuTK.Input; 8 12 9 13 namespace osu.Framework.Graphics.UserInterface 10 14 { 11 - public abstract class DropdownHeader : ClickableContainer 15 + public abstract class DropdownHeader : ClickableContainer, IKeyBindingHandler<PlatformAction> 12 16 { 17 + public event Action<DropdownSelectionAction> ChangeSelection; 18 + 13 19 protected Container Background; 14 20 protected Container Foreground; 15 21 ··· 71 77 { 72 78 Background.Colour = BackgroundColour; 73 79 base.OnHoverLost(e); 80 + } 81 + 82 + public override bool HandleNonPositionalInput => IsHovered; 83 + 84 + protected override bool OnKeyDown(KeyDownEvent e) 85 + { 86 + switch (e.Key) 87 + { 88 + case Key.Up: 89 + ChangeSelection?.Invoke(DropdownSelectionAction.Previous); 90 + return true; 91 + 92 + case Key.Down: 93 + ChangeSelection?.Invoke(DropdownSelectionAction.Next); 94 + return true; 95 + 96 + default: 97 + return base.OnKeyDown(e); 98 + } 99 + } 100 + 101 + public bool OnPressed(PlatformAction action) 102 + { 103 + switch (action.ActionType) 104 + { 105 + case PlatformActionType.ListStart: 106 + ChangeSelection?.Invoke(DropdownSelectionAction.First); 107 + return true; 108 + 109 + case PlatformActionType.ListEnd: 110 + ChangeSelection?.Invoke(DropdownSelectionAction.Last); 111 + return true; 112 + 113 + default: 114 + return false; 115 + } 116 + } 117 + 118 + public bool OnReleased(PlatformAction action) => false; 119 + 120 + public enum DropdownSelectionAction 121 + { 122 + Previous, 123 + Next, 124 + First, 125 + Last, 126 + FirstVisible, 127 + LastVisible 74 128 } 75 129 } 76 130 }
+1 -1
osu.Framework/Graphics/UserInterface/Menu.cs
··· 54 54 /// <summary> 55 55 /// Gets the item representations contained by this <see cref="Menu"/>. 56 56 /// </summary> 57 - protected IReadOnlyList<DrawableMenuItem> Children => ItemsContainer; 57 + protected internal IReadOnlyList<DrawableMenuItem> Children => ItemsContainer; 58 58 59 59 protected readonly Direction Direction; 60 60
+2
osu.Framework/Input/PlatformActionContainer.cs
··· 57 57 LineEnd, 58 58 DocumentPrevious, 59 59 DocumentNext, 60 + ListStart, 61 + ListEnd, 60 62 Save 61 63 } 62 64
+3 -1
osu.Framework/Platform/GameHost.cs
··· 936 936 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.PageDown), new PlatformAction(PlatformActionType.DocumentNext)), 937 937 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentNext)), 938 938 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentPrevious)), 939 - new KeyBinding(new KeyCombination(InputKey.Control, InputKey.S), new PlatformAction(PlatformActionType.Save)) 939 + new KeyBinding(new KeyCombination(InputKey.Control, InputKey.S), new PlatformAction(PlatformActionType.Save)), 940 + new KeyBinding(InputKey.Home, new PlatformAction(PlatformActionType.ListStart, PlatformActionMethod.Move)), 941 + new KeyBinding(InputKey.End, new PlatformAction(PlatformActionType.ListEnd, PlatformActionMethod.Move)) 940 942 }; 941 943 942 944 /// <summary>
+3 -1
osu.Framework/Platform/MacOS/MacOSGameHost.cs
··· 54 54 new KeyBinding(new KeyCombination(InputKey.Alt, InputKey.Super, InputKey.Right), new PlatformAction(PlatformActionType.DocumentNext)), 55 55 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentNext)), 56 56 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentPrevious)), 57 - new KeyBinding(new KeyCombination(InputKey.Super, InputKey.S), new PlatformAction(PlatformActionType.Save)) 57 + new KeyBinding(new KeyCombination(InputKey.Super, InputKey.S), new PlatformAction(PlatformActionType.Save)), 58 + new KeyBinding(new KeyCombination(InputKey.Super, InputKey.Up), new PlatformAction(PlatformActionType.ListStart, PlatformActionMethod.Move)), 59 + new KeyBinding(new KeyCombination(InputKey.Super, InputKey.Down), new PlatformAction(PlatformActionType.ListEnd, PlatformActionMethod.Move)) 58 60 }; 59 61 } 60 62 }