18 private int _minimumWidth;
20 internal MenuDropDownWidget? ParentMenu {
get;
private set; }
21 internal MenuDropDownWidget? OpenChild {
get;
private set; }
22 internal MenuDropDownWidget ActiveMenu => OpenChild?.ActiveMenu ??
this;
23 internal MenuItemWidget? SelectedItem => _selectedIndex >= 0 && _selectedIndex < _items.Count ? _items[_selectedIndex] :
null;
24 internal bool CanInvoke => !_disposed && _bar.IsAvailable && (ParentItem?.CanInvoke ??
true);
25 private readonly List<Entry> _entries =
new();
26 private readonly List<MenuItemWidget> _items =
new();
28 private EventHandler<DirectDrawingBase>? _closeCompletionHandler;
29 private int _nextChildNicknameSuffix;
30 private int _selectedIndex = -1;
31 private bool _disposed;
42 _theme = theme ??
throw new ArgumentNullException(nameof(theme));
43 _minimumWidth =
Width = Math.Max(_theme.MinimumDropDownWidth, _theme.DefaultDropDownWidth);
46 _theme.DropDownBackgroundColor,
49 new Rectangle(location.X, location.Y,
Width, 1),
56 Panel.HideWhenFullyTransparent =
false;
71 public IReadOnlyList<MenuItemWidget>
Items => _items.AsReadOnly();
74 public int Width {
get;
private set; }
77 public int Height {
get;
private set; }
80 public bool IsOpen {
get;
private set; }
95 public MenuDropDownWidget
AddItem(
string text, Action? action =
null,
96 string? shortcutText =
null,
bool enabled =
true,
string? key =
null,
97 KeyGesture? shortcut =
null,
char? mnemonic =
null, SKImage? icon =
null)
99 AddEntry(text, action, shortcutText, enabled, key, shortcut, mnemonic, icon);
113 public MenuDropDownWidget
AddCheckItem(
string text, Action<bool>? action =
null,
114 bool isChecked =
false,
bool enabled =
true,
string? key =
null,
115 KeyGesture? shortcut =
null,
char? mnemonic =
null, SKImage? icon =
null)
117 AddEntry(text,
null,
null, enabled, key, shortcut, mnemonic, icon,
118 checkable:
true, checkedAction: action).SetChecked(isChecked);
133 public MenuDropDownWidget
AddRadioItem(
string text, Action? action,
string group,
134 bool isChecked =
false,
bool enabled =
true,
string? key =
null,
135 KeyGesture? shortcut =
null,
char? mnemonic =
null, SKImage? icon =
null)
137 ArgumentException.ThrowIfNullOrWhiteSpace(group);
138 AddEntry(text, action,
null, enabled, key, shortcut, mnemonic, icon,
139 checkable:
true, radioGroup: group).SetChecked(isChecked);
151 public MenuDropDownWidget
AddSubMenu(
string text, Action<MenuDropDownWidget> configure,
152 bool enabled =
true,
string? key =
null,
char? mnemonic =
null, SKImage? icon =
null)
154 ArgumentNullException.ThrowIfNull(configure);
155 ValidateEntry(text, key,
null, mnemonic);
157 CreateChildNickname(
"submenu"), _bar);
161 var item = AddEntry(text,
null,
null, enabled, key,
null, mnemonic, icon, subMenu: child);
162 child.ParentMenu =
this;
163 child.ParentItem = item;
164 Add(child, Vector2.Zero);
166 child.SetDropDownZOrder(_zOrder + 10);
176 private void ValidateEntry(
string text,
string? key,
KeyGesture? shortcut,
char? mnemonic)
178 ObjectDisposedException.ThrowIf(_disposed,
this);
179 ArgumentNullException.ThrowIfNull(text);
180 _bar.ValidateRegistration(key, shortcut);
181 if (mnemonic is
char character && _items.Any(item =>
182 item.Mnemonic is
char existing &&
char.ToUpperInvariant(existing) ==
char.ToUpperInvariant(character)))
183 throw new ArgumentException($
"Duplicate menu mnemonic '{character}'.", nameof(mnemonic));
186 private MenuItemWidget AddEntry(
string text, Action? action,
string? shortcutText,
187 bool enabled,
string? key, KeyGesture? shortcut,
char? mnemonic, SKImage? icon,
188 bool checkable =
false,
string? radioGroup =
null, Action<bool>? checkedAction =
null,
189 MenuDropDownWidget? subMenu =
null)
191 ValidateEntry(text, key, shortcut, mnemonic);
194 new Rectangle(anchor.X, anchor.Y,
Width, _theme.ItemHeight), text, shortcutText,
195 action, _theme, CreateChildNickname(
"item"),
this, key, shortcut, mnemonic,
196 icon, checkable, radioGroup, checkedAction, subMenu);
197 item.SetEnabled(enabled);
198 item.Hovered += OnItemHovered;
200 _entries.Add(Entry.ForItem(item));
201 Add(item, Vector2.Zero);
202 _bar.RegisterItem(item);
205 item.ApplyInputState(
false);
209 SetDropDownZOrder(_zOrder);
213 internal void UncheckRadioPeers(MenuItemWidget selected)
215 foreach (var item
in _items)
216 if (!ReferenceEquals(item, selected) && item.RadioGroup == selected.RadioGroup && item.IsChecked)
217 item.SetChecked(
false);
220 private string CreateChildNickname(
string kind) => $
"{Nickname}.{kind}.{_nextChildNicknameSuffix++}";
222 internal void UnregisterItem(MenuItemWidget item)
224 _bar.UnregisterItem(item);
225 if (_disposed)
return;
226 OnItemDisabled(item);
227 item.SubMenu?.Dispose();
228 item.Hovered -= OnItemHovered;
230 _entries.RemoveAll(entry => ReferenceEquals(entry.Item, item));
232 foreach (var remaining
in _items) remaining.SetSelected(
false);
236 internal void OnItemDisabled(MenuItemWidget item)
238 if (item.SubMenu is not
null && ReferenceEquals(OpenChild, item.SubMenu)) CloseChild();
239 if (ReferenceEquals(SelectedItem, item)) SetSelectedIndex(-1);
242 internal void NotifyInvoked(MenuItemWidget item) => _bar.OnItemInvoked(item);
244 internal void OpenSubMenu(MenuItemWidget item,
bool selectFirst)
246 if (!
IsOpen || !item.CanInvoke || item.SubMenu is not { } child)
return;
247 SetSelectedIndex(_items.IndexOf(item));
248 if (!ReferenceEquals(OpenChild, child))
251 var viewport =
View!.Viewport.TargetRectPx;
252 var row = item.Background.ScreenBounds;
253 int x = Panel.ScreenBounds.Right + _theme.SubMenuGap;
254 if (x + child.Width > viewport.Right)
255 x = Panel.ScreenBounds.Left - child.Width - _theme.SubMenuGap;
256 x = Math.Clamp(x, viewport.Left, Math.Max(viewport.Left, viewport.Right - child.Width));
257 int y = Math.Clamp(row.Top, viewport.Top, Math.Max(viewport.Top, viewport.Bottom - child.Height));
260 child.OpenAnimated(_bar.DropDownAnimation, Math.Max(0, _bar.DropDownAnimationDurationSec));
263 if (selectFirst) child.SelectFirstEnabled();
266 internal void CloseChild()
268 var child = OpenChild;
277 foreach (var item
in _items)
286 SetEntryInputEnabled(
false);
287 CancelPendingClose();
289 base.ProcessHidden();
296 ObjectDisposedException.ThrowIf(_disposed,
this);
299 _theme.SeparatorColor,
302 new Rectangle(anchor.X, anchor.Y, 1, 1),
303 CreateChildNickname(
"separator"))
307 separator.HideWhenFullyTransparent =
false;
308 _entries.Add(Entry.ForSeparator(separator));
309 Add(separator, keepCurrentOffset:
false, explicitLocalOffsetPx: Vector2.Zero);
310 separator.Visible =
IsOpen;
311 separator.ZOrder = _zOrder + 2;
322 ObjectDisposedException.ThrowIf(_disposed,
this);
323 if (width < _theme.MinimumDropDownWidth)
325 throw new ArgumentOutOfRangeException(
328 $
"Menu dropdown width must be at least {_theme.MinimumDropDownWidth} pixels.");
331 _minimumWidth = width;
339 CancelPendingClose();
346 SetEntryInputEnabled(
true);
348 PrepareVisualsForAnimation();
353 SetRevealProgress(1f);
359 foreach (var visual
in EnumerateVisuals()) visual.FadeIn(durationSec);
366 .
RevealTo(1f, durationSec, EaseOutCubic);
373 bool immediate =
false)
378 SetEntryInputEnabled(
false);
379 SetSelectedIndex(-1);
380 CancelPendingClose();
386 SetRevealProgress(1f);
394 _closeCompletionHandler = (_, _) =>
396 CancelPendingClose();
402 Panel.FadeToCompleted += _closeCompletionHandler;
406 SetRevealProgress(1f);
407 foreach (var visual
in EnumerateVisuals()) visual.FadeOut(durationSec);
410 internal void SetDropDownZOrder(
int zOrder)
413 Panel.ZOrder = zOrder;
414 foreach (var item
in _items) item.SubMenu?.SetDropDownZOrder(zOrder + 10);
416 foreach (Entry entry
in _entries)
418 if (entry.Item is not
null)
419 entry.Item.SetItemZOrder(zOrder + 1);
420 else if (entry.Separator is not
null)
421 entry.Separator.ZOrder = zOrder + 2;
425 internal void SelectFirstEnabled()
427 SetSelectedIndex(FindNextEnabledIndex(-1, 1));
430 internal void SelectNextEnabled()
432 SetSelectedIndex(FindNextEnabledIndex(_selectedIndex, 1));
435 internal void SelectPreviousEnabled()
437 int start = _selectedIndex < 0 ? _items.Count : _selectedIndex;
438 SetSelectedIndex(FindNextEnabledIndex(start, -1));
441 internal void InvokeSelectedItem()
443 if (_selectedIndex < 0 || _selectedIndex >= _items.Count)
446 _items[_selectedIndex].PerformClick();
457 CancelPendingClose();
463 item.Hovered -= OnItemHovered;
469 private void RecalculateLayout()
471 int markerWidth = _items.Any(item => item.
IsCheckable) ? _theme.MarkerColumnWidth : 0;
472 int iconWidth = _items.Any(item => item.
Icon is not
null) ? _theme.IconSize + _theme.IconGap : 0;
473 int arrowWidth = _items.Any(item => item.
SubMenu is not
null) ? _theme.SubMenuArrowWidth : 0;
474 int shortcutWidth = _items.Select(item => MeasureText(item.
ShortcutText)).DefaultIfEmpty().Max();
475 int labelWidth = _items.Select(item => MeasureText(item.
Text)).DefaultIfEmpty().Max();
476 Width = Math.Max(_minimumWidth, markerWidth + iconWidth + arrowWidth + shortcutWidth + labelWidth +
477 (shortcutWidth > 0 ? _theme.ShortcutGap : 0) + 2 * (_theme.DropDownHorizontalPadding + _theme.ItemHorizontalPadding));
478 int y = _theme.DropDownVerticalPadding;
480 foreach (Entry entry
in _entries)
482 if (entry.Item is not
null)
484 var offset =
new Vector2(_theme.DropDownHorizontalPadding, y);
487 Point position = Point.Round(
new PointF(
491 entry.Item.SetBounds(
new Rectangle(
494 Width - (_theme.DropDownHorizontalPadding * 2),
495 _theme.ItemHeight), markerWidth, iconWidth, shortcutWidth, arrowWidth);
497 y += _theme.ItemHeight;
501 if (entry.Separator is not
null)
503 int lineX = _theme.DropDownHorizontalPadding + 5;
504 int lineY = y + (_theme.SeparatorHeight / 2);
505 var offset =
new Vector2(lineX, lineY);
509 Point position = Point.Round(
new PointF(
513 entry.Separator.ScreenBounds =
new Rectangle(
516 Math.Max(1,
Width - (lineX * 2)),
519 y += _theme.SeparatorHeight;
523 Height = Math.Max(1, y + _theme.DropDownVerticalPadding);
526 Panel.ScreenBounds =
new Rectangle(panelPosition.X, panelPosition.Y,
Width,
Height);
529 private void SetEntryInputEnabled(
bool menuAcceptsInput)
531 foreach (MenuItemWidget item
in _items)
532 item.ApplyInputState(menuAcceptsInput);
535 private void SetSelectedIndex(
int index)
537 if (index < -1 || index >= _items.Count)
540 if (_selectedIndex == index)
543 if (_selectedIndex >= 0 && _selectedIndex < _items.Count)
544 _items[_selectedIndex].SetSelected(
false);
547 _selectedIndex = index;
549 if (_selectedIndex >= 0)
550 _items[_selectedIndex].SetSelected(
true);
553 private int FindNextEnabledIndex(
int startIndex,
int direction)
555 if (_items.Count == 0)
558 int index = startIndex;
560 for (
int attempt = 0; attempt < _items.Count; attempt++)
562 index = (index + direction + _items.Count) % _items.Count;
564 if (_items[index].CanInvoke)
571 private void OnItemHovered(MenuItemWidget item)
573 int index = _items.IndexOf(item);
577 SetSelectedIndex(index);
578 if (item.SubMenu is not
null) OpenSubMenu(item, selectFirst:
false);
582 private int MeasureText(
string text)
584 using var font =
new SKFont(SKTypeface.Default, _theme.FontSize);
585 return (
int)Math.Ceiling(font.MeasureText(text));
588 private void PrepareVisualsForAnimation()
590 foreach (DirectDrawingBase visual
in EnumerateVisuals())
593 visual.HideWhenFullyTransparent =
false;
597 private void CancelVisualFades()
599 foreach (DirectDrawingBase visual
in EnumerateVisuals())
603 private void SetRevealProgress(
float progress)
605 foreach (DirectDrawingBase visual
in EnumerateVisuals())
613 private IEnumerable<DirectDrawingBase> EnumerateVisuals()
615 return EnumerateVisuals(
this);
618 private static IEnumerable<DirectDrawingBase> EnumerateVisuals(IDirectCompositeContainer container)
620 foreach (IDirectCompositeChild child
in container.
Children)
622 if (child is DirectDrawingBase drawing)
623 yield
return drawing;
625 if (child is IDirectCompositeContainer nestedContainer && child is not MenuDropDownWidget)
627 foreach (DirectDrawingBase nestedDrawing
in EnumerateVisuals(nestedContainer))
628 yield
return nestedDrawing;
633 private void CancelPendingClose()
635 if (_closeCompletionHandler is
null)
638 Panel.FadeToCompleted -= _closeCompletionHandler;
639 _closeCompletionHandler =
null;
642 private static float EaseOutCubic(
float t)
644 float inverse = 1f - t;
645 return 1f - (inverse * inverse * inverse);
648 private sealed class Entry
650 private Entry(MenuItemWidget? item,
651 DirectRectangle? separator)
654 Separator = separator;
657 internal MenuItemWidget? Item {
get; }
658 internal DirectRectangle? Separator {
get; }
660 internal static Entry ForItem(MenuItemWidget item) =>
new(item,
null);
661 internal static Entry ForSeparator(DirectRectangle separator) =>
new(
null, separator);