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 AddAssert("correct count", () => count == 2); 273 } 274 275 private class TestContainer : Container 276 { 277 public new void ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay);
··· 272 AddAssert("correct count", () => count == 2); 273 } 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 + 299 private class TestContainer : Container 300 { 301 public new void ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay);
+219 -9
osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs
··· 6 using System.Linq; 7 using NUnit.Framework; 8 using osu.Framework.Bindables; 9 using osu.Framework.Graphics.UserInterface; 10 using osu.Framework.Input.Events; 11 using osu.Framework.Testing; 12 using osuTK; 13 using osuTK.Input; ··· 19 private const int items_to_add = 10; 20 private const float explicit_height = 100; 21 private float calculatedHeight; 22 - private readonly TestDropdown testDropdown, testDropdownMenu, bindableDropdown; 23 private readonly BindableList<string> bindableList = new BindableList<string>(); 24 25 public override IReadOnlyList<Type> RequiredTypes => new[] 26 { 27 typeof(Dropdown<>), ··· 41 while (i < items_to_add) 42 testItems[i] = @"test " + i++; 43 44 - Add(testDropdown = new TestDropdown 45 { 46 - Width = 150, 47 - Position = new Vector2(200, 70), 48 - Items = testItems 49 }); 50 51 - Add(testDropdownMenu = new TestDropdown 52 { 53 - Width = 150, 54 - Position = new Vector2(400, 70), 55 - Items = testItems 56 }); 57 58 Add(bindableDropdown = new TestDropdown 59 { 60 Width = 150, 61 Position = new Vector2(600, 70), 62 ItemSource = bindableList 63 }); 64 } 65 ··· 121 AddAssert("current value should be two", () => bindableDropdown.Current.Value == "two"); 122 } 123 124 [Test] 125 public void SelectNull() 126 { ··· 151 public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)? 152 .TriggerEvent(new ClickEvent(GetContainingInputManager().CurrentState, MouseButton.Left)); 153 } 154 } 155 } 156 }
··· 6 using System.Linq; 7 using NUnit.Framework; 8 using osu.Framework.Bindables; 9 + using osu.Framework.Graphics; 10 using osu.Framework.Graphics.UserInterface; 11 + using osu.Framework.Input; 12 using osu.Framework.Input.Events; 13 + using osu.Framework.Input.States; 14 using osu.Framework.Testing; 15 using osuTK; 16 using osuTK.Input; ··· 22 private const int items_to_add = 10; 23 private const float explicit_height = 100; 24 private float calculatedHeight; 25 + private readonly TestDropdown testDropdown, testDropdownMenu, bindableDropdown, emptyDropdown; 26 + private readonly PlatformActionContainer platformActionContainerKeyboardSelection, platformActionContainerKeyboardPreselection, platformActionContainerEmptyDropdown; 27 private readonly BindableList<string> bindableList = new BindableList<string>(); 28 29 + private int previousIndex; 30 + private int lastVisibleIndexOnTheCurrentPage, lastVisibleIndexOnTheNextPage; 31 + private int firstVisibleIndexOnTheCurrentPage, firstVisibleIndexOnThePreviousPage; 32 + 33 public override IReadOnlyList<Type> RequiredTypes => new[] 34 { 35 typeof(Dropdown<>), ··· 49 while (i < items_to_add) 50 testItems[i] = @"test " + i++; 51 52 + Add(platformActionContainerKeyboardSelection = new PlatformActionContainer 53 { 54 + Child = testDropdown = new TestDropdown 55 + { 56 + Width = 150, 57 + Position = new Vector2(200, 70), 58 + Items = testItems 59 + } 60 }); 61 62 + Add(platformActionContainerKeyboardPreselection = new PlatformActionContainer 63 { 64 + Child = testDropdownMenu = new TestDropdown 65 + { 66 + Width = 150, 67 + Position = new Vector2(400, 70), 68 + Items = testItems 69 + } 70 }); 71 + testDropdownMenu.Menu.MaxHeight = explicit_height; 72 73 Add(bindableDropdown = new TestDropdown 74 { 75 Width = 150, 76 Position = new Vector2(600, 70), 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 + } 87 }); 88 } 89 ··· 145 AddAssert("current value should be two", () => bindableDropdown.Current.Value == "two"); 146 } 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 + 329 [Test] 330 public void SelectNull() 331 { ··· 356 public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)? 357 .TriggerEvent(new ClickEvent(GetContainingInputManager().CurrentState, MouseButton.Left)); 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); 364 } 365 } 366 }
+21
osu.Framework/Graphics/Containers/Container.cs
··· 43 internalChildrenAsT = (IReadOnlyList<T>)InternalChildren; 44 else 45 internalChildrenAsT = new LazyList<Drawable, T>(InternalChildren, c => (T)c); 46 } 47 48 /// <summary> ··· 73 return internalChildrenAsT; 74 } 75 set => ChildrenEnumerable = value; 76 } 77 78 /// <summary> ··· 142 } 143 144 private readonly IReadOnlyList<T> internalChildrenAsT; 145 146 /// <summary> 147 /// The index of a given child within <see cref="Children"/>.
··· 43 internalChildrenAsT = (IReadOnlyList<T>)InternalChildren; 44 else 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); 51 } 52 53 /// <summary> ··· 78 return internalChildrenAsT; 79 } 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 + } 96 } 97 98 /// <summary> ··· 162 } 163 164 private readonly IReadOnlyList<T> internalChildrenAsT; 165 + private readonly IReadOnlyList<T> aliveInternalChildrenAsT; 166 167 /// <summary> 168 /// The index of a given child within <see cref="Children"/>.
+191 -6
osu.Framework/Graphics/UserInterface/Dropdown.cs
··· 6 using System.Linq; 7 using osu.Framework.Bindables; 8 using osu.Framework.Extensions; 9 using osu.Framework.Graphics.Containers; 10 using osuTK.Graphics; 11 - using osu.Framework.Extensions.IEnumerableExtensions; 12 - using osu.Framework.Graphics.Sprites; 13 14 namespace osu.Framework.Graphics.UserInterface 15 { ··· 204 Menu.RelativeSizeAxes = Axes.X; 205 206 Header.Action = Menu.Toggle; 207 Current.ValueChanged += selectionChanged; 208 209 ItemSource.ItemsAdded += _ => setItems(ItemSource); 210 ItemSource.ItemsRemoved += _ => setItems(ItemSource); 211 } 212 213 protected override void LoadComplete() ··· 289 290 #region DropdownMenu 291 292 - public abstract class DropdownMenu : Menu 293 { 294 protected DropdownMenu() 295 : base(Direction.Vertical) 296 { 297 } 298 299 /// <summary> 300 /// Selects an item from this <see cref="DropdownMenu"/>. 301 /// </summary> 302 /// <param name="item">The item to select.</param> 303 public void SelectItem(DropdownMenuItem<T> item) 304 { 305 - Children.OfType<DrawableDropdownMenuItem>().ForEach(c => c.IsSelected = c.Item == item); 306 } 307 308 /// <summary> ··· 322 /// </summary> 323 public bool AnyPresent => Children.Any(c => c.IsPresent); 324 325 protected sealed override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => CreateDrawableDropdownMenuItem(item); 326 327 protected abstract DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item); ··· 331 // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 332 public abstract class DrawableDropdownMenuItem : DrawableMenuItem 333 { 334 protected DrawableDropdownMenuItem(MenuItem item) 335 : base(item) 336 { ··· 352 } 353 } 354 355 private Color4 backgroundColourSelected = Color4.SlateGray; 356 357 public Color4 BackgroundColourSelected ··· 387 388 protected override void UpdateBackgroundColour() 389 { 390 - Background.FadeColour(IsHovered ? BackgroundColourHover : IsSelected ? BackgroundColourSelected : BackgroundColour); 391 } 392 393 protected override void UpdateForegroundColour() 394 { 395 - Foreground.FadeColour(IsHovered ? ForegroundColourHover : IsSelected ? ForegroundColourSelected : ForegroundColour); 396 } 397 398 protected override void LoadComplete() ··· 401 Background.Colour = IsSelected ? BackgroundColourSelected : BackgroundColour; 402 Foreground.Colour = IsSelected ? ForegroundColourSelected : ForegroundColour; 403 } 404 } 405 406 #endregion 407 } 408 409 #endregion
··· 6 using System.Linq; 7 using osu.Framework.Bindables; 8 using osu.Framework.Extensions; 9 + using osu.Framework.Extensions.IEnumerableExtensions; 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; 16 using osuTK.Graphics; 17 + using osuTK.Input; 18 19 namespace osu.Framework.Graphics.UserInterface 20 { ··· 209 Menu.RelativeSizeAxes = Axes.X; 210 211 Header.Action = Menu.Toggle; 212 + Header.ChangeSelection += selectionKeyPressed; 213 + Menu.PreselectionConfirmed += preselectionConfirmed; 214 Current.ValueChanged += selectionChanged; 215 216 ItemSource.ItemsAdded += _ => setItems(ItemSource); 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 + } 254 } 255 256 protected override void LoadComplete() ··· 332 333 #region DropdownMenu 334 335 + public abstract class DropdownMenu : Menu, IKeyBindingHandler<PlatformAction> 336 { 337 protected DropdownMenu() 338 : base(Direction.Vertical) 339 { 340 + StateChanged += clearPreselection; 341 } 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 + 365 /// <summary> 366 /// Selects an item from this <see cref="DropdownMenu"/>. 367 /// </summary> 368 /// <param name="item">The item to select.</param> 369 public void SelectItem(DropdownMenuItem<T> item) 370 { 371 + Children.OfType<DrawableDropdownMenuItem>().ForEach(c => 372 + { 373 + c.IsSelected = c.Item == item; 374 + if (c.IsSelected) 375 + ContentContainer.ScrollIntoView(c); 376 + }); 377 } 378 379 /// <summary> ··· 393 /// </summary> 394 public bool AnyPresent => Children.Any(c => c.IsPresent); 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 + 412 protected sealed override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => CreateDrawableDropdownMenuItem(item); 413 414 protected abstract DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item); ··· 418 // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 419 public abstract class DrawableDropdownMenuItem : DrawableMenuItem 420 { 421 + public event Action<DropdownMenuItem<T>> PreselectionRequested; 422 + 423 protected DrawableDropdownMenuItem(MenuItem item) 424 : base(item) 425 { ··· 441 } 442 } 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 + 464 private Color4 backgroundColourSelected = Color4.SlateGray; 465 466 public Color4 BackgroundColourSelected ··· 496 497 protected override void UpdateBackgroundColour() 498 { 499 + Background.FadeColour(IsPreSelected ? BackgroundColourHover : IsSelected ? BackgroundColourSelected : BackgroundColour); 500 } 501 502 protected override void UpdateForegroundColour() 503 { 504 + Foreground.FadeColour(IsPreSelected ? ForegroundColourHover : IsSelected ? ForegroundColourSelected : ForegroundColour); 505 } 506 507 protected override void LoadComplete() ··· 510 Background.Colour = IsSelected ? BackgroundColourSelected : BackgroundColour; 511 Foreground.Colour = IsSelected ? ForegroundColourSelected : ForegroundColour; 512 } 513 + 514 + protected override bool OnHover(HoverEvent e) 515 + { 516 + PreselectionRequested?.Invoke(Item as DropdownMenuItem<T>); 517 + return base.OnHover(e); 518 + } 519 } 520 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; 592 } 593 594 #endregion
+55 -1
osu.Framework/Graphics/UserInterface/DropdownHeader.cs
··· 2 // See the LICENCE file in the repository root for full licence text. 3 4 using osuTK.Graphics; 5 using osu.Framework.Graphics.Containers; 6 using osu.Framework.Graphics.Shapes; 7 using osu.Framework.Input.Events; 8 9 namespace osu.Framework.Graphics.UserInterface 10 { 11 - public abstract class DropdownHeader : ClickableContainer 12 { 13 protected Container Background; 14 protected Container Foreground; 15 ··· 71 { 72 Background.Colour = BackgroundColour; 73 base.OnHoverLost(e); 74 } 75 } 76 }
··· 2 // See the LICENCE file in the repository root for full licence text. 3 4 using osuTK.Graphics; 5 + using System; 6 using osu.Framework.Graphics.Containers; 7 using osu.Framework.Graphics.Shapes; 8 + using osu.Framework.Input; 9 + using osu.Framework.Input.Bindings; 10 using osu.Framework.Input.Events; 11 + using osuTK.Input; 12 13 namespace osu.Framework.Graphics.UserInterface 14 { 15 + public abstract class DropdownHeader : ClickableContainer, IKeyBindingHandler<PlatformAction> 16 { 17 + public event Action<DropdownSelectionAction> ChangeSelection; 18 + 19 protected Container Background; 20 protected Container Foreground; 21 ··· 77 { 78 Background.Colour = BackgroundColour; 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 128 } 129 } 130 }
+1 -1
osu.Framework/Graphics/UserInterface/Menu.cs
··· 54 /// <summary> 55 /// Gets the item representations contained by this <see cref="Menu"/>. 56 /// </summary> 57 - protected IReadOnlyList<DrawableMenuItem> Children => ItemsContainer; 58 59 protected readonly Direction Direction; 60
··· 54 /// <summary> 55 /// Gets the item representations contained by this <see cref="Menu"/>. 56 /// </summary> 57 + protected internal IReadOnlyList<DrawableMenuItem> Children => ItemsContainer; 58 59 protected readonly Direction Direction; 60
+2
osu.Framework/Input/PlatformActionContainer.cs
··· 57 LineEnd, 58 DocumentPrevious, 59 DocumentNext, 60 Save 61 } 62
··· 57 LineEnd, 58 DocumentPrevious, 59 DocumentNext, 60 + ListStart, 61 + ListEnd, 62 Save 63 } 64
+3 -1
osu.Framework/Platform/GameHost.cs
··· 936 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.PageDown), new PlatformAction(PlatformActionType.DocumentNext)), 937 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentNext)), 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)) 940 }; 941 942 /// <summary>
··· 936 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.PageDown), new PlatformAction(PlatformActionType.DocumentNext)), 937 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentNext)), 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)), 940 + new KeyBinding(InputKey.Home, new PlatformAction(PlatformActionType.ListStart, PlatformActionMethod.Move)), 941 + new KeyBinding(InputKey.End, new PlatformAction(PlatformActionType.ListEnd, PlatformActionMethod.Move)) 942 }; 943 944 /// <summary>
+3 -1
osu.Framework/Platform/MacOS/MacOSGameHost.cs
··· 54 new KeyBinding(new KeyCombination(InputKey.Alt, InputKey.Super, InputKey.Right), new PlatformAction(PlatformActionType.DocumentNext)), 55 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentNext)), 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)) 58 }; 59 } 60 }
··· 54 new KeyBinding(new KeyCombination(InputKey.Alt, InputKey.Super, InputKey.Right), new PlatformAction(PlatformActionType.DocumentNext)), 55 new KeyBinding(new KeyCombination(InputKey.Control, InputKey.Tab), new PlatformAction(PlatformActionType.DocumentNext)), 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)), 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)) 60 }; 61 } 62 }