Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 不兼容
Expand Down Expand Up @@ -218,6 +225,28 @@ class _HomePageState extends State<HomePage> 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)
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
),
],
);
Expand Down Expand Up @@ -217,6 +226,28 @@ class _HomePageState extends State<HomePage> 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)
8 changes: 8 additions & 0 deletions packages/tray_manager/example/lib/pages/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,35 @@ class _HomePageState extends State<HomePage> 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');
Expand All @@ -184,6 +191,7 @@ class _HomePageState extends State<HomePage> with TrayListener {
MenuItem.checkbox(
label: 'Item 2',
checked: false,
icon: 'images/tray_icon.png',
onClick: (menuItem) {
if (kDebugMode) {
print('click item 2');
Expand Down
50 changes: 49 additions & 1 deletion packages/tray_manager/lib/src/tray_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,59 @@ class TrayManager {
Future<void> setContextMenu(Menu menu) async {
_menu = menu;
final Map<String, dynamic> arguments = {
'menu': menu.toJson(),
'menu': await _menuToJsonForPlatform(menu),
};
await _channel.invokeMethod('setContextMenu', arguments);
}

Future<Map<String, dynamic>> _menuToJsonForPlatform(Menu menu) async {
if (defaultTargetPlatform != TargetPlatform.macOS) {
return menu.toJson();
}
return _menuToJsonWithBase64Icons(menu, <String, String>{});
}

Future<Map<String, dynamic>> _menuToJsonWithBase64Icons(
Menu menu,
Map<String, String> iconBase64Cache,
) async {
final items = menu.items ?? const <MenuItem>[];
final jsonItems = <Map<String, dynamic>>[];

for (final item in items) {
final m = Map<String, dynamic>.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 <String, dynamic>{
'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
Expand Down
23 changes: 23 additions & 0 deletions packages/tray_manager/macos/Classes/TrayMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
Binary file added screenshots/macos_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/macos_icon1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.