diff --git a/README-ZH.md b/README-ZH.md index 2403cfe..87ba00c 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -50,6 +50,13 @@ | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos.png?raw=true) | ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/linux.png?raw=true) | ![image](https://github.com/leanflutter/tray_manager/blob/main/screenshots/windows.png?raw=true) | +### 菜单项图标(macOS) + +通过 `MenuItem.icon` 渲染的菜单项图标: + +![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon.png?raw=true) +![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon1.png?raw=true) + ## 已知问题 ### 与 app_links 不兼容 @@ -218,6 +225,28 @@ class _HomePageState extends State with TrayListener { | popUpContextMenu | 弹出托盘图标的上下文菜单。 | ➖ | ✔️ | ✔️ | | getBounds | 返回 `Rect` 这个托盘图标的边界。 | ➖ | ✔️ | ✔️ | +## 菜单项图标(macOS) + +`MenuItem`(来自 `menu_base`)带有 `icon` 字段。在 **macOS** 上,如果你传入 Flutter assets 路径,就可以显示每个菜单项的图标: + +```dart +Menu menu = Menu( + items: [ + MenuItem( + key: 'show_window', + label: 'Show Window', + icon: 'images/tray_icon.png', + ), + ], +); +await trayManager.setContextMenu(menu); +``` + +说明: +- 插件会优先从 Flutter assets 加载图标,并以 base64 形式传给原生侧。 +- 如果 assets 加载失败,会回退把 `icon` 当作绝对文件路径来尝试加载(best-effort)。 +- 其他平台目前可能会忽略该字段。 + ## 许可证 [MIT](./LICENSE) diff --git a/README.md b/README.md index 53e3fc9..069e20c 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,13 @@ English | [简体中文](./README-ZH.md) | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos.png?raw=true) | ![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/linux.png?raw=true) | ![image](https://github.com/leanflutter/tray_manager/blob/main/screenshots/windows.png?raw=true) | +### MenuItem icon (macOS) + +Menu item icons rendered via `MenuItem.icon`: + +![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon.png?raw=true) +![](https://github.com/leanflutter/tray_manager/blob/main/screenshots/macos_icon1.png?raw=true) + ## Known Issues ### Not Working with app_links @@ -124,11 +131,13 @@ Menu menu = Menu( MenuItem( key: 'show_window', label: 'Show Window', + icon: 'images/tray_icon.png', ), MenuItem.separator(), MenuItem( key: 'exit_app', label: 'Exit App', + icon: 'images/tray_icon.png', ), ], ); @@ -217,6 +226,28 @@ class _HomePageState extends State with TrayListener { | popUpContextMenu | Pops up the context menu of the tray icon. | ➖ | ✔️ | ✔️ | | getBounds | Returns `Rect` The bounds of this tray icon. | ➖ | ✔️ | ✔️ | +## MenuItem icon (macOS) + +`MenuItem` has an `icon` field (from `menu_base`). On **macOS**, per-menu-item icons are supported when you provide an asset path: + +```dart +Menu menu = Menu( + items: [ + MenuItem( + key: 'show_window', + label: 'Show Window', + icon: 'images/tray_icon.png', + ), + ], +); +await trayManager.setContextMenu(menu); +``` + +Notes: +- The plugin will try to load the icon from Flutter assets and pass it to native code as base64. +- If the icon asset can't be loaded, it will fall back to treating `icon` as an absolute file path (best-effort). +- Other platforms may ignore this field for now. + ## License [MIT](./LICENSE) diff --git a/packages/tray_manager/example/lib/pages/home.dart b/packages/tray_manager/example/lib/pages/home.dart index 2d21789..b46869a 100644 --- a/packages/tray_manager/example/lib/pages/home.dart +++ b/packages/tray_manager/example/lib/pages/home.dart @@ -152,28 +152,35 @@ class _HomePageState extends State with TrayListener { items: [ MenuItem( label: 'Look Up "LeanFlutter"', + icon: 'images/tray_icon.png', ), MenuItem( label: 'Search with Google', + icon: 'images/tray_icon.png', ), MenuItem.separator(), MenuItem( label: 'Cut', + icon: 'images/tray_icon.png', ), MenuItem( label: 'Copy', + icon: 'images/tray_icon.png', ), MenuItem( label: 'Paste', disabled: true, + icon: 'images/tray_icon.png', ), MenuItem.submenu( label: 'Share', + icon: 'images/tray_icon.png', submenu: Menu( items: [ MenuItem.checkbox( label: 'Item 1', checked: true, + icon: 'images/tray_icon.png', onClick: (menuItem) { if (kDebugMode) { print('click item 1'); @@ -184,6 +191,7 @@ class _HomePageState extends State with TrayListener { MenuItem.checkbox( label: 'Item 2', checked: false, + icon: 'images/tray_icon.png', onClick: (menuItem) { if (kDebugMode) { print('click item 2'); diff --git a/packages/tray_manager/lib/src/tray_manager.dart b/packages/tray_manager/lib/src/tray_manager.dart index f23b61c..78af08e 100644 --- a/packages/tray_manager/lib/src/tray_manager.dart +++ b/packages/tray_manager/lib/src/tray_manager.dart @@ -180,11 +180,59 @@ class TrayManager { Future setContextMenu(Menu menu) async { _menu = menu; final Map arguments = { - 'menu': menu.toJson(), + 'menu': await _menuToJsonForPlatform(menu), }; await _channel.invokeMethod('setContextMenu', arguments); } + Future> _menuToJsonForPlatform(Menu menu) async { + if (defaultTargetPlatform != TargetPlatform.macOS) { + return menu.toJson(); + } + return _menuToJsonWithBase64Icons(menu, {}); + } + + Future> _menuToJsonWithBase64Icons( + Menu menu, + Map iconBase64Cache, + ) async { + final items = menu.items ?? const []; + final jsonItems = >[]; + + for (final item in items) { + final m = Map.from(item.toJson()); + + final iconPath = item.icon; + if (iconPath != null && iconPath.isNotEmpty) { + final cached = iconBase64Cache[iconPath]; + if (cached != null) { + m['base64Icon'] = cached; + } else { + try { + final data = await rootBundle.load(iconPath); + final base64Icon = base64Encode(data.buffer.asUint8List()); + iconBase64Cache[iconPath] = base64Icon; + m['base64Icon'] = base64Icon; + } catch (_) { + // Best-effort: if icon isn't a bundled asset or can't be loaded, + // leave it as-is (platform side may still support file paths). + } + } + } + + final submenu = item.submenu; + if (submenu != null) { + m['submenu'] = await _menuToJsonWithBase64Icons(submenu, iconBase64Cache); + } + + jsonItems.add(m); + } + + return { + 'items': jsonItems, + }..removeWhere((key, value) => value == null); + } + /// Pops up the context menu of the tray icon. /// /// [bringAppToFront] If true, the app will be brought to the front when the diff --git a/packages/tray_manager/macos/Classes/TrayMenu.swift b/packages/tray_manager/macos/Classes/TrayMenu.swift index 3119693..ff4ea0f 100644 --- a/packages/tray_manager/macos/Classes/TrayMenu.swift +++ b/packages/tray_manager/macos/Classes/TrayMenu.swift @@ -10,6 +10,26 @@ import AppKit public class TrayMenu: NSMenu, NSMenuDelegate { public var onMenuItemClick:((NSMenuItem) -> Void)? + private static let menuIconSize = NSSize(width: 16, height: 16) + + private static func loadMenuItemImage(from itemDict: [String: Any]) -> NSImage? { + if let base64Icon = itemDict["base64Icon"] as? String, + let data = Data(base64Encoded: base64Icon), + let image = NSImage(data: data) { + image.size = menuIconSize + return image + } + // Fallback: treat `icon` as an absolute file path if provided. + if let iconPath = itemDict["icon"] as? String, + !iconPath.isEmpty, + FileManager.default.fileExists(atPath: iconPath), + let image = NSImage(contentsOfFile: iconPath) { + image.size = menuIconSize + return image + } + return nil + } + public override init(title: String) { super.init(title: title) } @@ -42,6 +62,9 @@ public class TrayMenu: NSMenu, NSMenuDelegate { menuItem.tag = id menuItem.title = label menuItem.toolTip = toolTip + if let image = TrayMenu.loadMenuItemImage(from: itemDict) { + menuItem.image = image + } menuItem.isEnabled = !disabled menuItem.action = !disabled ? #selector(statusItemMenuButtonClicked) : nil menuItem.target = self diff --git a/screenshots/macos_icon.png b/screenshots/macos_icon.png new file mode 100644 index 0000000..a8802df Binary files /dev/null and b/screenshots/macos_icon.png differ diff --git a/screenshots/macos_icon1.png b/screenshots/macos_icon1.png new file mode 100644 index 0000000..3e6365a Binary files /dev/null and b/screenshots/macos_icon1.png differ