import "package:flutter/widgets.dart"; sealed class AppMenuItem { const AppMenuItem(); } class AppMenuGroup extends AppMenuItem { const AppMenuGroup({ required this.label, required this.children, this.icon, this.visible = true, }); final String label; final IconData? icon; final List children; final bool visible; List get visibleChildren => children.where((item) { return switch (item) { AppMenuGroup group => group.visible, AppMenuAction action => action.visible, _ => true, }; }).toList(); } class AppMenuAction extends AppMenuItem { const AppMenuAction({ required this.label, this.icon, this.trailingIcon, this.onTap, this.shortcut, this.visible = true, }); final String label; final IconData? icon; final IconData? trailingIcon; final VoidCallback? onTap; final SingleActivator? shortcut; final bool visible; bool get enabled => onTap != null; } class AppMenuCheck extends AppMenuItem { const AppMenuCheck({ required this.label, required this.checked, this.checkedLabel, this.onToggle, this.shortcut, }); final String label; final String? checkedLabel; final bool checked; final VoidCallback? onToggle; final SingleActivator? shortcut; } class AppMenuPlatformProvided extends AppMenuItem { const AppMenuPlatformProvided(this.type); final PlatformProvidedMenuItemType type; } class AppMenuSeparator extends AppMenuItem { const AppMenuSeparator({this.visible = true}); final bool visible; } /// Converts the shared menu model to Flutter's native platform menu model. class AppMenuNativeRenderer { static List build( List menus, { String appName = "Garage App", }) { final appItems = [ for (final type in [ PlatformProvidedMenuItemType.about, PlatformProvidedMenuItemType.servicesSubmenu, PlatformProvidedMenuItemType.hide, PlatformProvidedMenuItemType.hideOtherApplications, PlatformProvidedMenuItemType.showAllApplications, PlatformProvidedMenuItemType.quit, ]) if (PlatformProvidedMenuItem.hasMenu(type)) PlatformProvidedMenuItem(type: type), ]; return [ if (appItems.isNotEmpty) PlatformMenu(label: appName, menus: appItems), for (final group in menus) if (group.visible) PlatformMenu( label: group.label, menus: _convertItems(group.visibleChildren), ), ]; } static String signature(List menus) { final buffer = StringBuffer(); for (final group in menus) { if (!group.visible) continue; _writeSignature(buffer, group); buffer.write(";"); } return buffer.toString(); } static List _convertItems(List items) { final groups = >[[]]; for (final item in items) { if (item case AppMenuSeparator(visible: true)) { groups.add([]); } else if (item is! AppMenuSeparator) { groups.last.add(item); } } final result = []; for (var index = 0; index < groups.length; index++) { final native = groups[index] .map(_toNativeItem) .whereType(); final items = native.toList(); if (items.isEmpty) continue; if (index == 0) { result.addAll(items); } else { result.add(PlatformMenuItemGroup(members: items)); } } return result; } static PlatformMenuItem? _toNativeItem(AppMenuItem item) { return switch (item) { AppMenuGroup group when group.visible => PlatformMenu( label: group.label, menus: _convertItems(group.visibleChildren), ), AppMenuAction action when action.visible => PlatformMenuItem( label: action.label, shortcut: action.shortcut, onSelected: action.onTap, ), AppMenuCheck check => PlatformMenuItem( label: check.checked && check.checkedLabel != null ? check.checkedLabel! : check.label, shortcut: check.shortcut, onSelected: check.onToggle, ), AppMenuPlatformProvided provided when PlatformProvidedMenuItem.hasMenu(provided.type) => PlatformProvidedMenuItem(type: provided.type), _ => null, }; } static void _writeSignature(StringBuffer buffer, AppMenuItem item) { switch (item) { case AppMenuGroup group: if (!group.visible) return; buffer ..write("G(") ..write(group.label) ..write("|") ..write(group.icon?.codePoint ?? -1) ..write(")["); for (final child in group.visibleChildren) { _writeSignature(buffer, child); buffer.write(","); } buffer.write("]"); case AppMenuAction action: if (!action.visible) return; buffer ..write("A(") ..write(action.label) ..write("|") ..write(action.icon?.codePoint ?? -1) ..write("|") ..write(action.enabled ? 1 : 0) ..write("|") ..write(_shortcutSignature(action.shortcut)) ..write(")"); case AppMenuCheck check: buffer ..write("C(") ..write(check.label) ..write("|") ..write(check.checkedLabel ?? "-") ..write("|") ..write(check.checked ? 1 : 0) ..write("|") ..write(check.onToggle != null ? 1 : 0) ..write("|") ..write(_shortcutSignature(check.shortcut)) ..write(")"); case AppMenuPlatformProvided provided: buffer ..write("P(") ..write(provided.type.name) ..write(")"); case AppMenuSeparator separator: if (separator.visible) buffer.write("S"); } } // SingleActivator has no value-based toString (it's Diagnosticable, so the // default one embeds the object's identity hash) - and menu_def.dart builds // a fresh activator on every rebuild, so hashing the instance itself would // make the signature change every frame and defeat the whole point of this // gate. build it off the actual key/modifier fields instead, those are what // we care about being stable. static String _shortcutSignature(SingleActivator? shortcut) { if (shortcut == null) return "-"; return [ shortcut.trigger.keyLabel, shortcut.trigger.keyId.toRadixString(16), shortcut.alt ? "a" : "", shortcut.control ? "c" : "", shortcut.meta ? "m" : "", shortcut.shift ? "s" : "", ].join(":"); } }